diff --git a/.drone.yml b/.drone.yml index 16a432cf3fa42..e02b91e7420c0 100644 --- a/.drone.yml +++ b/.drone.yml @@ -39,7 +39,7 @@ steps: - ./libraries/vendor/bin/phan - name: npm - image: node:16-bullseye-slim + image: node:18-bullseye-slim depends_on: [ phpcs ] volumes: - name: npm-cache @@ -372,6 +372,6 @@ trigger: --- kind: signature -hmac: 6b06b1c7f407650fe98f0851dc865911f399422116fa4f250a52d01a556397ed +hmac: 45b19b7430edc5ec922ef32c2f2dcb284c7cbf7ba55eb295ba4d877cee0fe5a4 ... diff --git a/administrator/components/com_finder/src/Controller/IndexController.php b/administrator/components/com_finder/src/Controller/IndexController.php index 138983dda93db..bddc94bb51fc3 100644 --- a/administrator/components/com_finder/src/Controller/IndexController.php +++ b/administrator/components/com_finder/src/Controller/IndexController.php @@ -82,7 +82,9 @@ public function purge() $this->checkToken(); // Remove the script time limit. - @set_time_limit(0); + if (\function_exists('set_time_limit')) { + set_time_limit(0); + } /** @var \Joomla\Component\Finder\Administrator\Model\IndexModel $model */ $model = $this->getModel('Index', 'Administrator'); diff --git a/administrator/components/com_finder/src/Controller/IndexerController.php b/administrator/components/com_finder/src/Controller/IndexerController.php index 89a59ac241b83..6fa43f72bb89d 100644 --- a/administrator/components/com_finder/src/Controller/IndexerController.php +++ b/administrator/components/com_finder/src/Controller/IndexerController.php @@ -147,7 +147,9 @@ public function batch() ob_start(); // Remove the script time limit. - @set_time_limit(0); + if (\function_exists('set_time_limit')) { + set_time_limit(0); + } // Get the indexer state. $state = Indexer::getState(); diff --git a/administrator/components/com_scheduler/src/Model/TasksModel.php b/administrator/components/com_scheduler/src/Model/TasksModel.php index 95f3cc0170365..bac44530eff41 100644 --- a/administrator/components/com_scheduler/src/Model/TasksModel.php +++ b/administrator/components/com_scheduler/src/Model/TasksModel.php @@ -11,6 +11,7 @@ namespace Joomla\Component\Scheduler\Administrator\Model; use Joomla\CMS\Component\ComponentHelper; +use Joomla\CMS\Date\Date; use Joomla\CMS\Factory; use Joomla\CMS\Language\Text; use Joomla\CMS\MVC\Factory\MVCFactoryInterface; @@ -366,7 +367,7 @@ 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; + $listDirectionN = strtolower($this->getState('list.direction', 'asc')) === 'desc' ? -1 : 1; // Set limit parameters and get object list $query->setLimit($limit, $limitstart); @@ -395,7 +396,7 @@ static function (array $arr) { $this->attachTaskOptions($responseList); // If ordering by non-db fields, we need to sort here in code - if ($listOrder == 'j.type_title') { + if ($listOrder === 'j.type_title') { $responseList = ArrayHelper::sortObjects($responseList, 'safeTypeTitle', $listDirectionN, true, false); } @@ -437,4 +438,34 @@ protected function populateState($ordering = 'a.title', $direction = 'ASC'): voi // Call the parent method parent::populateState($ordering, $direction); } + + /** + * Check if we have any enabled due tasks and no locked tasks. + * + * @param Date $time The next execution time to check against + * + * @return boolean + * @since __DEPLOY_VERSION__ + */ + public function hasDueTasks(Date $time): bool + { + $db = $this->getDatabase(); + $now = $time->toSql(); + + $query = $db->getQuery(true) + // Count due tasks + ->select('SUM(CASE WHEN ' . $db->quoteName('a.next_execution') . ' <= :now THEN 1 ELSE 0 END) AS due_count') + // Count locked tasks + ->select('SUM(CASE WHEN ' . $db->quoteName('a.locked') . ' IS NULL THEN 0 ELSE 1 END) AS locked_count') + ->from($db->quoteName('#__scheduler_tasks', 'a')) + ->where($db->quoteName('a.state') . ' = 1') + ->bind(':now', $now); + + $db->setQuery($query); + + $taskDetails = $db->loadObject(); + + // False if we don't have due tasks, or we have locked tasks + return $taskDetails && $taskDetails->due_count && !$taskDetails->locked_count; + } } diff --git a/administrator/components/com_users/forms/group.xml b/administrator/components/com_users/forms/group.xml index 5d22be594bc8c..ab07760580862 100644 --- a/administrator/components/com_users/forms/group.xml +++ b/administrator/components/com_users/forms/group.xml @@ -5,7 +5,6 @@ name="id" type="hidden" default="0" - required="true" readonly="true" /> diff --git a/administrator/language/en-GB/com_actionlogs.sys.ini b/administrator/language/en-GB/com_actionlogs.sys.ini index ef7ee6c78f570..8b66db209ac16 100644 --- a/administrator/language/en-GB/com_actionlogs.sys.ini +++ b/administrator/language/en-GB/com_actionlogs.sys.ini @@ -6,4 +6,4 @@ COM_ACTIONLOGS="User Actions Log" COM_ACTIONLOGS_VIEW_DEFAULT_DESC="Shows a list of user actions." COM_ACTIONLOGS_VIEW_DEFAULT_TITLE="User Action Log" -COM_ACTIONLOGS_XML_DESCRIPTION="Displays a log of actions performed by users on your website." \ No newline at end of file +COM_ACTIONLOGS_XML_DESCRIPTION="Displays a log of actions performed by users on your website." diff --git a/administrator/language/en-GB/com_associations.sys.ini b/administrator/language/en-GB/com_associations.sys.ini index 9e7d978ab5a83..ea124624a0773 100644 --- a/administrator/language/en-GB/com_associations.sys.ini +++ b/administrator/language/en-GB/com_associations.sys.ini @@ -4,4 +4,4 @@ ; Note : All ini files need to be saved as UTF-8 COM_ASSOCIATIONS="Multilingual Associations" -COM_ASSOCIATIONS_XML_DESCRIPTION="Improved multilingual content management component" \ No newline at end of file +COM_ASSOCIATIONS_XML_DESCRIPTION="Improved multilingual content management component" diff --git a/administrator/language/en-GB/plg_content_joomla.sys.ini b/administrator/language/en-GB/plg_content_joomla.sys.ini index f1617db909a52..bb15f45c376d1 100644 --- a/administrator/language/en-GB/plg_content_joomla.sys.ini +++ b/administrator/language/en-GB/plg_content_joomla.sys.ini @@ -4,4 +4,4 @@ ; Note : All ini files need to be saved as UTF-8 PLG_CONTENT_JOOMLA="Content - Joomla" -PLG_CONTENT_JOOMLA_XML_DESCRIPTION="This plugin does category processing for core extensions; sends an email when new article is submitted in the Frontend." \ No newline at end of file +PLG_CONTENT_JOOMLA_XML_DESCRIPTION="This plugin does category processing for core extensions; sends an email when new article is submitted in the Frontend." diff --git a/administrator/language/en-GB/plg_content_vote.sys.ini b/administrator/language/en-GB/plg_content_vote.sys.ini index 089d239acf156..ef4b09ab0575e 100644 --- a/administrator/language/en-GB/plg_content_vote.sys.ini +++ b/administrator/language/en-GB/plg_content_vote.sys.ini @@ -4,4 +4,4 @@ ; Note : All ini files need to be saved as UTF-8 PLG_CONTENT_VOTE="Content - Vote" -PLG_VOTE_XML_DESCRIPTION="Add Voting functionality to Articles." \ No newline at end of file +PLG_VOTE_XML_DESCRIPTION="Add Voting functionality to Articles." diff --git a/administrator/language/en-GB/plg_extension_joomla.sys.ini b/administrator/language/en-GB/plg_extension_joomla.sys.ini index c461cdb61c03c..9c0e64f4be4f9 100644 --- a/administrator/language/en-GB/plg_extension_joomla.sys.ini +++ b/administrator/language/en-GB/plg_extension_joomla.sys.ini @@ -4,4 +4,4 @@ ; Note : All ini files need to be saved as UTF-8 PLG_EXTENSION_JOOMLA="Extension - Joomla" -PLG_EXTENSION_JOOMLA_XML_DESCRIPTION="Manage the update sites for extensions." \ No newline at end of file +PLG_EXTENSION_JOOMLA_XML_DESCRIPTION="Manage the update sites for extensions." diff --git a/administrator/language/en-GB/plg_extension_namespacemap.ini b/administrator/language/en-GB/plg_extension_namespacemap.ini index d32aa7ebe1df7..8c95b93561d55 100644 --- a/administrator/language/en-GB/plg_extension_namespacemap.ini +++ b/administrator/language/en-GB/plg_extension_namespacemap.ini @@ -4,4 +4,4 @@ ; Note : All ini files need to be saved as UTF-8 PLG_EXTENSION_NAMESPACEMAP="Extension - Namespace Updater" -PLG_EXTENSION_NAMESPACEMAP_XML_DESCRIPTION="

Automatically builds and updates the administrator/cache/autoload_psr4.php file that is used to autoload extensions.

Warning! This plugin must be enabled as it runs on extension install, update and uninstall.

" \ No newline at end of file +PLG_EXTENSION_NAMESPACEMAP_XML_DESCRIPTION="

Automatically builds and updates the administrator/cache/autoload_psr4.php file that is used to autoload extensions.

Warning! This plugin must be enabled as it runs on extension install, update and uninstall.

" diff --git a/administrator/language/en-GB/plg_system_sef.sys.ini b/administrator/language/en-GB/plg_system_sef.sys.ini index db6f969889d14..a9fbc4a7ab8f4 100644 --- a/administrator/language/en-GB/plg_system_sef.sys.ini +++ b/administrator/language/en-GB/plg_system_sef.sys.ini @@ -4,4 +4,4 @@ ; Note : All ini files need to be saved as UTF-8 PLG_SEF_XML_DESCRIPTION="Adds SEF support to links in the document. It operates directly on the HTML and does not require a special tag." -PLG_SYSTEM_SEF="System - SEF" \ No newline at end of file +PLG_SYSTEM_SEF="System - SEF" diff --git a/administrator/language/en-GB/plg_user_terms.sys.ini b/administrator/language/en-GB/plg_user_terms.sys.ini index 138ff07a90e5c..a5ac184576ace 100644 --- a/administrator/language/en-GB/plg_user_terms.sys.ini +++ b/administrator/language/en-GB/plg_user_terms.sys.ini @@ -4,4 +4,4 @@ ; Note : All ini files need to be saved as UTF-8 PLG_USER_TERMS="User - Terms and Conditions" -PLG_USER_TERMS_XML_DESCRIPTION="Basic plugin to request user's consent to the site's terms and conditions." \ No newline at end of file +PLG_USER_TERMS_XML_DESCRIPTION="Basic plugin to request user's consent to the site's terms and conditions." diff --git a/api/components/com_modules/src/Controller/ModulesController.php b/api/components/com_modules/src/Controller/ModulesController.php index f394794088395..31066a3ac2c30 100644 --- a/api/components/com_modules/src/Controller/ModulesController.php +++ b/api/components/com_modules/src/Controller/ModulesController.php @@ -53,7 +53,7 @@ class ModulesController extends ApiController */ public function displayItem($id = null) { - $this->modelState->set('filter.client_id', $this->getClientIdFromInput()); + $this->modelState->set('client_id', $this->getClientIdFromInput()); return parent::displayItem($id); } @@ -67,7 +67,7 @@ public function displayItem($id = null) */ public function displayList() { - $this->modelState->set('filter.client_id', $this->getClientIdFromInput()); + $this->modelState->set('client_id', $this->getClientIdFromInput()); return parent::displayList(); } diff --git a/api/components/com_templates/src/Controller/StylesController.php b/api/components/com_templates/src/Controller/StylesController.php index 87903695145b6..e973c2bfb50fc 100644 --- a/api/components/com_templates/src/Controller/StylesController.php +++ b/api/components/com_templates/src/Controller/StylesController.php @@ -88,7 +88,7 @@ protected function preprocessSaveData(array $data): array // 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'); + unset($data['template']); } $model = $this->getModel(Inflector::singularize($this->contentType), '', ['ignore_request' => true]); diff --git a/build/bump.php b/build/bump.php index 69212ec12d41f..46a16c13149bf 100644 --- a/build/bump.php +++ b/build/bump.php @@ -59,6 +59,8 @@ function usage($command) $antJobFile = '/build.xml'; +$packageJsonFile = '/package.json'; + $readMeFiles = [ '/README.md', '/README.txt', @@ -245,6 +247,15 @@ function usage($command) file_put_contents($rootPath . $antJobFile, $fileContents); } +// Updates the version in the package.json file. +if (file_exists($rootPath . $packageJsonFile)) { + $package = json_decode(file_get_contents($rootPath . $packageJsonFile)); + $package->version = $version['release']; + + // @todo use a native formatter whenever https://github.com/php/php-src/issues/8864 is resolved + file_put_contents($rootPath . $packageJsonFile, str_replace(' ', ' ', json_encode($package, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES))); +} + // Updates the version in readme files. foreach ($readMeFiles as $readMeFile) { if (file_exists($rootPath . $readMeFile)) { diff --git a/build/media_source/system/css/fields/calendar-rtl.css b/build/media_source/system/css/fields/calendar-rtl.css index d22f9db4d6c51..674d226738fe7 100644 --- a/build/media_source/system/css/fields/calendar-rtl.css +++ b/build/media_source/system/css/fields/calendar-rtl.css @@ -2,9 +2,11 @@ * @copyright (C) 2016 Open Source Matters, Inc. * @license GNU General Public License version 2 or later; see LICENSE.txt */ - .js-calendar { + +.js-calendar { box-shadow: 0 0 15px 4px rgba(0,0,0,.15) !important; - } +} + .calendar-container { float: left; min-width: 160px; @@ -14,13 +16,23 @@ background-color: #ffffff !important; z-index: 1100 !important; } + +.calendar-container .nav { + display: table-cell; +} + .calendar-container table { table-layout: fixed; - max-width: 262px; + max-width: 268px; border-radius: 5px; background-color: #ffffff !important; z-index: 1100 !important; + margin-top: 2px; + margin-left: auto; + margin-right: auto; + padding: 3px; } + /* The main calendar widget. DIV containing a table. */ div.calendar-container table th, .calendar-container table td { box-shadow: none; @@ -100,6 +112,7 @@ div.calendar-container table td.title { /* This holds the current "month, year" width: auto; font-weight: bold; } + .calendar-container table tbody td.today:after { position: absolute; bottom: 3px; @@ -110,6 +123,7 @@ div.calendar-container table td.title { /* This holds the current "month, year" border-radius: 1.5px; background-color: #46a546; } + .calendar-container table tbody td.today.selected:after { background-color: #fff; } @@ -119,6 +133,7 @@ div.calendar-container table td.title { /* This holds the current "month, year" background: #3d8fd7; color: #fff; } + .calendar-container table tbody td.day:hover:after { background-color: #fff; } @@ -135,42 +150,78 @@ div.calendar-container table td.title { /* This holds the current "month, year" .calendar-container table tbody .emptyrow { /* Empty row (some months need less than 6 rows) */ display: none; } + .calendar-container .calendar-head-row td { padding: 4px 0 !important; border-bottom: none; } + .calendar-container .day-name { + padding-top: 0.5rem; font-size: 0.7rem; font-weight: bold; + border-bottom: none; } + .calendar-container .time td { - padding: 8px 8px 8px 0; + padding: 15px 3px 10px 0; + border-bottom: none; +} + +.calendar-container td.time-title { + display: block; + margin-top: 20px; } + +.calendar-container .time td select { + display: block; + width: 100%; + padding: 5px 9px 3px; + font-size: 16px; + font-weight: 400; + line-height: 1.5; + color: #212529; + background-color: #f0f4fb; + background-image: url("../../images/select-bg-rtl.svg"); + background-repeat: no-repeat; + background-position: left center; + background-size: max(100%, 58rem); + border: 1px solid #cdcdcd; + border-radius: 0.25rem; + appearance: none; +} + .buttons-wrapper { - padding: 5px 5px; - width:100%; + margin-bottom: 0 !important; + padding: 5px; + width: 100%; } + .buttons-wrapper .btn { min-width: 60px; color: #495057; border: 1px solid #495057; - margin-left: .5rem; + margin-left: 0; padding: 0 16px; line-height: 2.375rem; box-shadow: 1px 0 1px 1px rgba(0,0,0,.25); } + .buttons-wrapper .btn:hover { color: #fff; background: #1a466b; } + .buttons-wrapper .btn:last-child { margin-left: 0; } + .time .time-title { - background-image: url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='utf-8'%3F%3E%3Csvg width='22' height='22' viewBox='0 0 1792 1792' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1024 544v448q0 14-9 23t-23 9h-320q-14 0-23-9t-9-23v-64q0-14 9-23t23-9h224v-352q0-14 9-23t23-9h64q14 0 23 9t9 23zm416 352q0-148-73-273t-198-198-273-73-273 73-198 198-73 273 73 273 198 198 273 73 273-73 198-198 73-273zm224 0q0 209-103 385.5t-279.5 279.5-385.5 103-385.5-103-279.5-279.5-103-385.5 103-385.5 279.5-279.5 385.5-103 385.5 103 279.5 279.5 103 385.5z'/%3E%3C/svg%3E"); + background-image: url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='utf-8'%3F%3E%3Csvg width='24' height='24' viewBox='0 0 1792 1792' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1024 544v448q0 14-9 23t-23 9h-320q-14 0-23-9t-9-23v-64q0-14 9-23t23-9h224v-352q0-14 9-23t23-9h64q14 0 23 9t9 23zm416 352q0-148-73-273t-198-198-273-73-273 73-198 198-73 273 73 273 198 198 273 73 273-73 198-198 73-273zm224 0q0 209-103 385.5t-279.5 279.5-385.5 103-385.5-103-279.5-279.5-103-385.5 103-385.5 279.5-279.5 385.5-103 385.5 103 279.5 279.5 103 385.5z'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: center; } + /* Fix cursor on js-btn and time select */ .calendar-container select, .calendar-container .js-btn { diff --git a/build/media_source/system/css/fields/calendar.css b/build/media_source/system/css/fields/calendar.css index 70dc7aa09bac3..038567b481fca 100644 --- a/build/media_source/system/css/fields/calendar.css +++ b/build/media_source/system/css/fields/calendar.css @@ -2,9 +2,11 @@ * @copyright (C) 2016 Open Source Matters, Inc. * @license GNU General Public License version 2 or later; see LICENSE.txt */ - .js-calendar { + +.js-calendar { box-shadow: 0 0 15px 4px rgba(0,0,0,.15) !important; - } +} + .calendar-container { float: left; min-width: 160px; @@ -14,13 +16,23 @@ background-color: #ffffff !important; z-index: 1100 !important; } + +.calendar-container .nav { + display: table-cell; +} + .calendar-container table { table-layout: fixed; - max-width: 262px; + max-width: 268px; border-radius: 5px; background-color: #ffffff !important; z-index: 1100 !important; + margin-top: 2px; + margin-left: auto; + margin-right: auto; + padding: 3px; } + /* The main calendar widget. DIV containing a table. */ div.calendar-container table th, .calendar-container table td { box-shadow: none; @@ -100,6 +112,7 @@ div.calendar-container table td.title { /* This holds the current "month, year" width: auto; font-weight: bold; } + .calendar-container table tbody td.today:after { position: absolute; bottom: 3px; @@ -110,6 +123,7 @@ div.calendar-container table td.title { /* This holds the current "month, year" border-radius: 1.5px; background-color: #46a546; } + .calendar-container table tbody td.today.selected:after { background-color: #fff; } @@ -119,6 +133,7 @@ div.calendar-container table td.title { /* This holds the current "month, year" background: #3d8fd7; color: #fff; } + .calendar-container table tbody td.day:hover:after { background-color: #fff; } @@ -140,38 +155,73 @@ div.calendar-container table td.title { /* This holds the current "month, year" padding: 4px 0 !important; border-bottom: none; } + .calendar-container .day-name { + padding-top: 0.5rem; font-size: 0.7rem; font-weight: bold; + border-bottom: none; } + .calendar-container .time td { - padding: 8px 0 8px 8px; + padding: 15px 3px 10px 0; + border-bottom: none; +} + +.calendar-container td.time-title { + display: block; + margin-top: 20px; } + +.calendar-container .time td select { + display: block; + width: 100%; + padding: 5px 9px 3px; + font-size: 16px; + font-weight: 400; + line-height: 1.5; + color: #212529; + background-color: #f0f4fb; + background-image: url("../../images/select-bg.svg"); + background-repeat: no-repeat; + background-position: right center; + background-size: max(100%, 58rem); + border: 1px solid #cdcdcd; + border-radius: 0.25rem; + appearance: none; +} + .buttons-wrapper { - padding: 5px 5px; - width:100%; + margin-bottom: 0 !important; + padding: 5px; + width: 100%; } + .buttons-wrapper .btn { min-width: 60px; color: #495057; border: 1px solid #495057; - margin-right: .5rem; + margin-right: 0; padding: 0 16px; line-height: 2.375rem; box-shadow: 1px 1px 1px 0 rgba(0,0,0,.25); } + .buttons-wrapper .btn:hover { color: #fff; background: #1a466b; } + .buttons-wrapper .btn:last-child { margin-right: 0; } + .time .time-title { - background-image: url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='utf-8'%3F%3E%3Csvg width='22' height='22' viewBox='0 0 1792 1792' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1024 544v448q0 14-9 23t-23 9h-320q-14 0-23-9t-9-23v-64q0-14 9-23t23-9h224v-352q0-14 9-23t23-9h64q14 0 23 9t9 23zm416 352q0-148-73-273t-198-198-273-73-273 73-198 198-73 273 73 273 198 198 273 73 273-73 198-198 73-273zm224 0q0 209-103 385.5t-279.5 279.5-385.5 103-385.5-103-279.5-279.5-103-385.5 103-385.5 279.5-279.5 385.5-103 385.5 103 279.5 279.5 103 385.5z'/%3E%3C/svg%3E"); + background-image: url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='utf-8'%3F%3E%3Csvg width='24' height='24' viewBox='0 0 1792 1792' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1024 544v448q0 14-9 23t-23 9h-320q-14 0-23-9t-9-23v-64q0-14 9-23t23-9h224v-352q0-14 9-23t23-9h64q14 0 23 9t9 23zm416 352q0-148-73-273t-198-198-273-73-273 73-198 198-73 273 73 273 198 198 273 73 273-73 198-198 73-273zm224 0q0 209-103 385.5t-279.5 279.5-385.5 103-385.5-103-279.5-279.5-103-385.5 103-385.5 279.5-279.5 385.5-103 385.5 103 279.5 279.5 103 385.5z'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: center; } + /* Fix cursor on js-btn and time select */ .calendar-container select, .calendar-container .js-btn { diff --git a/build/media_source/system/images/select-bg-rtl.svg b/build/media_source/system/images/select-bg-rtl.svg new file mode 100644 index 0000000000000..a1ae9a3e60bb1 --- /dev/null +++ b/build/media_source/system/images/select-bg-rtl.svg @@ -0,0 +1 @@ + diff --git a/build/media_source/system/images/select-bg.svg b/build/media_source/system/images/select-bg.svg new file mode 100644 index 0000000000000..e2fefccf30242 --- /dev/null +++ b/build/media_source/system/images/select-bg.svg @@ -0,0 +1 @@ + diff --git a/build/media_source/system/js/fields/calendar.es5.js b/build/media_source/system/js/fields/calendar.es5.js index 2ab7a308c9005..b4bc2721020b4 100644 --- a/build/media_source/system/js/fields/calendar.es5.js +++ b/build/media_source/system/js/fields/calendar.es5.js @@ -632,7 +632,7 @@ row.className = "calendar-head-row"; this._nav_py = hh("‹", 1, -2, '', {"text-align": "center", "font-size": "18px", "line-height": "18px"}, 'js-btn btn-prev-year'); // Previous year button this.title = hh('
', this.params.weekNumbers ? 6 : 5, 300); - this.title.className = "title"; + this.title.className = "title title-year"; this._nav_ny = hh(" ›", 1, 2, '', {"text-align": "center", "font-size": "18px", "line-height": "18px"}, 'js-btn btn-next-year'); // Next year button } @@ -640,7 +640,7 @@ row.className = "calendar-head-row"; this._nav_pm = hh("‹", 1, -1, '', {"text-align": "center", "font-size": "2em", "line-height": "1em"}, 'js-btn btn-prev-month'); // Previous month button this._nav_month = hh('
', this.params.weekNumbers ? 6 : 5, 888, 'td', {'textAlign': 'center'}); - this._nav_month.className = "title"; + this._nav_month.className = "title title-month"; this._nav_nm = hh(" ›", 1, 1, '', {"text-align": "center", "font-size": "2em", "line-height": "1em"}, 'js-btn btn-next-month'); // Next month button row = createElement("tr", thead); // day names @@ -698,7 +698,7 @@ row = createElement("tr", tbody); row.className = "time"; - cell = createElement("td", row); + var cell = createElement("td", row); cell.className = "time time-title"; cell.colSpan = 1; cell.style.verticalAlign = 'middle'; @@ -706,11 +706,11 @@ var cell1 = createElement("td", row); cell1.className = "time hours-select"; - cell1.colSpan = 2; + cell1.colSpan = self.params.time24 ? 3 : 2; var cell2 = createElement("td", row); cell2.className = "time minutes-select"; - cell2.colSpan = 2; + cell2.colSpan = self.params.time24 ? 3 : 2; (function () { function makeTimePart(className, selected, range_start, range_end, cellTml) { @@ -750,11 +750,12 @@ M = makeTimePart("time time-minutes", mins, 0, 59, cell2), AP = null; - cell = createElement("td", row); - cell.className = "time ampm-select"; - cell.colSpan = self.params.weekNumbers ? 2 : 3; if (t12) { + cell = createElement("td", row); + cell.className = "time ampm-select"; + cell.colSpan = self.params.weekNumbers ? 3 : 2; + var selAttr = true, altDate = Date.parseFieldDate(self.inputField.getAttribute('data-alt-value'), self.params.dateFormat, 'gregorian', self.strings); pm = (altDate.getHours() >= 12); @@ -772,9 +773,10 @@ event.target.parentNode.parentNode.childNodes[2].childNodes[0].value, event.target.parentNode.parentNode.childNodes[3].childNodes[0].value); }, false); - } else { + } else if (self.params.weekNumbers) { + cell = createElement("td", row); cell.innerHTML = " "; - cell.colSpan = self.params.weekNumbers ? 3 : 2; + cell.colSpan = 1; } H.addEventListener("change", function (event) { diff --git a/components/com_wrapper/tmpl/wrapper/default.xml b/components/com_wrapper/tmpl/wrapper/default.xml index c9598fd7e622a..7c04e6653ac44 100644 --- a/components/com_wrapper/tmpl/wrapper/default.xml +++ b/components/com_wrapper/tmpl/wrapper/default.xml @@ -18,8 +18,6 @@ type="url" label="COM_WRAPPER_FIELD_URL_LABEL" required="true" - filter="url" - validate="url" /> diff --git a/composer.json b/composer.json index a33506822ae3a..a646e85f8de47 100644 --- a/composer.json +++ b/composer.json @@ -1,129 +1,129 @@ { - "name": "joomla/joomla-cms", - "type": "project", - "description": "Joomla CMS", - "keywords": [ - "joomla", - "cms" - ], - "homepage": "https://github.com/joomla/joomla-cms", - "license": "GPL-2.0-or-later", - "config": { - "optimize-autoloader": true, - "platform": { - "php": "8.1.0" - }, - "vendor-dir": "libraries/vendor", - "github-protocols": ["https"], - "allow-plugins": { - "dealerdirect/phpcodesniffer-composer-installer": true - } + "name": "joomla/joomla-cms", + "type": "project", + "description": "Joomla CMS", + "keywords": [ + "joomla", + "cms" + ], + "homepage": "https://github.com/joomla/joomla-cms", + "license": "GPL-2.0-or-later", + "config": { + "optimize-autoloader": true, + "platform": { + "php": "8.1.0" }, - "support": { - "issues": "https://issues.joomla.org/", - "irc": "irc://chat.freenode.net/joomla/", - "forum": "https://forum.joomla.org/", - "docs": "https://docs.joomla.org/" - }, - "repositories": [ - { - "type": "vcs", - "url": "https://github.com/joomla-backports/json-api-php.git", - "no-api": true - } - ], - "autoload": { - "psr-4": { - "Joomla\\CMS\\": "libraries/src/" - } - }, - "autoload-dev": { - "psr-4": { - "Joomla\\Tests\\": "tests" - } - }, - "require": { - "php": "^8.1.0", - "joomla/application": "~3.0.x-dev", - "joomla/archive": "~3.0.x-dev", - "joomla/authentication": "~3.0.x-dev", - "joomla/console": "~3.0.x-dev", - "joomla/crypt": "~3.0.x-dev", - "joomla/data": "~3.0.x-dev", - "joomla/database": "~3.0.x-dev", - "joomla/di": "~3.0.x-dev", - "joomla/event": "~3.0.x-dev", - "joomla/filter": "~3.0.x-dev", - "joomla/filesystem": "~3.0.x-dev", - "joomla/http": "~3.0.x-dev", - "joomla/input": "~3.0.x-dev", - "joomla/language": "~3.0.x-dev", - "joomla/oauth1": "~3.0.x-dev", - "joomla/oauth2": "~3.0.x-dev", - "joomla/registry": "~3.0.x-dev", - "joomla/router": "~3.0.x-dev", - "joomla/session": "~3.0.x-dev", - "joomla/string": "~3.0.x-dev", - "joomla/uri": "~3.0.x-dev", - "joomla/utilities": "~3.0.x-dev", - "algo26-matthias/idna-convert": "^3.1.0", - "defuse/php-encryption": "^2.4.0", - "doctrine/inflector": "^1.4.4", - "fig/link-util": "^1.2.0", - "google/recaptcha": "^1.3.0", - "laminas/laminas-diactoros": "^2.25.2", - "paragonie/sodium_compat": "^1.20", - "phpmailer/phpmailer": "^6.8.1", - "psr/link": "~1.1.1", - "symfony/console": "^6.3.4", - "symfony/error-handler": "^6.3.2", - "symfony/ldap": "^6.3.0", - "symfony/options-resolver": "^6.3.0", - "symfony/polyfill-mbstring": "^1.28.0", - "symfony/web-link": "^6.3.0", - "symfony/yaml": "^6.3.3", - "typo3/phar-stream-wrapper": "^3.1.7", - "wamania/php-stemmer": "^3.0.1", - "maximebf/debugbar": "^1.18.2", - "tobscure/json-api": "dev-joomla-backports", - "willdurand/negotiation": "^3.1.0", - "ext-json": "*", - "ext-simplexml": "*", - "psr/log": "~3.0", - "ext-gd": "*", - "web-auth/webauthn-lib": "4.3.0", - "ext-dom": "*", - "composer/ca-bundle": "^1.3.7", - "dragonmantank/cron-expression": "^3.3.3", - "enshrined/svg-sanitize": "^0.15.4", - "lcobucci/jwt": "^4.3.0", - "web-token/signature-pack": "^3.2.8", - "phpseclib/bcmath_compat": "^2.0.1", - "jfcherng/php-diff": "^6.15.3", - "voku/portable-utf8": "^6.0.13" - }, - "require-dev": { - "phpunit/phpunit": "^9.6.11", - "friendsofphp/php-cs-fixer": "~3.4.0", - "squizlabs/php_codesniffer": "~3.7.2", - "dealerdirect/phpcodesniffer-composer-installer": "^0.7.2", - "joomla/mediawiki": "^3.0.x-dev", - "joomla/test": "~3.0.x-dev", - "phan/phan": "^5.4.2" - }, - "replace": { - "paragonie/random_compat": "9.99.99", - "symfony/polyfill-php70": "*", - "symfony/polyfill-php71": "*", - "symfony/polyfill-php72": "*", - "symfony/polyfill-php73": "*", - "symfony/polyfill-php74": "*", - "symfony/polyfill-php80": "*", - "symfony/polyfill-php81": "*" - }, - "scripts": { - "post-install-cmd": [ - "php build/update_fido_cache.php" - ] + "vendor-dir": "libraries/vendor", + "github-protocols": ["https"], + "allow-plugins": { + "dealerdirect/phpcodesniffer-composer-installer": true + } + }, + "support": { + "issues": "https://issues.joomla.org/", + "irc": "irc://chat.freenode.net/joomla/", + "forum": "https://forum.joomla.org/", + "docs": "https://docs.joomla.org/" + }, + "repositories": [ + { + "type": "vcs", + "url": "https://github.com/joomla-backports/json-api-php.git", + "no-api": true + } + ], + "autoload": { + "psr-4": { + "Joomla\\CMS\\": "libraries/src/" + } + }, + "autoload-dev": { + "psr-4": { + "Joomla\\Tests\\": "tests" } + }, + "require": { + "php": "^8.1.0", + "joomla/application": "~3.0.x-dev", + "joomla/archive": "~3.0.x-dev", + "joomla/authentication": "~3.0.x-dev", + "joomla/console": "~3.0.x-dev", + "joomla/crypt": "~3.0.x-dev", + "joomla/data": "~3.0.x-dev", + "joomla/database": "~3.0.x-dev", + "joomla/di": "~3.0.x-dev", + "joomla/event": "~3.0.x-dev", + "joomla/filter": "~3.0.x-dev", + "joomla/filesystem": "~3.0.x-dev", + "joomla/http": "~3.0.x-dev", + "joomla/input": "~3.0.x-dev", + "joomla/language": "~3.0.x-dev", + "joomla/oauth1": "~3.0.x-dev", + "joomla/oauth2": "~3.0.x-dev", + "joomla/registry": "~3.0.x-dev", + "joomla/router": "~3.0.x-dev", + "joomla/session": "~3.0.x-dev", + "joomla/string": "~3.0.x-dev", + "joomla/uri": "~3.0.x-dev", + "joomla/utilities": "~3.0.x-dev", + "algo26-matthias/idna-convert": "^3.1.0", + "defuse/php-encryption": "^2.4.0", + "doctrine/inflector": "^1.4.4", + "fig/link-util": "^1.2.0", + "google/recaptcha": "^1.3.0", + "laminas/laminas-diactoros": "^2.25.2", + "paragonie/sodium_compat": "^1.20", + "phpmailer/phpmailer": "^6.8.1", + "psr/link": "~1.1.1", + "symfony/console": "^6.3.4", + "symfony/error-handler": "^6.3.2", + "symfony/ldap": "^6.3.0", + "symfony/options-resolver": "^6.3.0", + "symfony/polyfill-mbstring": "^1.28.0", + "symfony/web-link": "^6.3.0", + "symfony/yaml": "^6.3.3", + "typo3/phar-stream-wrapper": "^3.1.7", + "wamania/php-stemmer": "^3.0.1", + "maximebf/debugbar": "^1.18.2", + "tobscure/json-api": "dev-joomla-backports", + "willdurand/negotiation": "^3.1.0", + "ext-json": "*", + "ext-simplexml": "*", + "psr/log": "~3.0", + "ext-gd": "*", + "web-auth/webauthn-lib": "4.3.0", + "ext-dom": "*", + "composer/ca-bundle": "^1.3.7", + "dragonmantank/cron-expression": "^3.3.3", + "enshrined/svg-sanitize": "^0.15.4", + "lcobucci/jwt": "^4.3.0", + "web-token/signature-pack": "^3.2.8", + "phpseclib/bcmath_compat": "^2.0.1", + "jfcherng/php-diff": "^6.15.3", + "voku/portable-utf8": "^6.0.13" + }, + "require-dev": { + "phpunit/phpunit": "^9.6.11", + "friendsofphp/php-cs-fixer": "~3.4.0", + "squizlabs/php_codesniffer": "~3.7.2", + "dealerdirect/phpcodesniffer-composer-installer": "^0.7.2", + "joomla/mediawiki": "^3.0.x-dev", + "joomla/test": "~3.0.x-dev", + "phan/phan": "^5.4.2" + }, + "replace": { + "paragonie/random_compat": "9.99.99", + "symfony/polyfill-php70": "*", + "symfony/polyfill-php71": "*", + "symfony/polyfill-php72": "*", + "symfony/polyfill-php73": "*", + "symfony/polyfill-php74": "*", + "symfony/polyfill-php80": "*", + "symfony/polyfill-php81": "*" + }, + "scripts": { + "post-install-cmd": [ + "php build/update_fido_cache.php" + ] + } } diff --git a/language/en-GB/com_users.ini b/language/en-GB/com_users.ini index fb506aeb48408..2ff24bcfa8d52 100644 --- a/language/en-GB/com_users.ini +++ b/language/en-GB/com_users.ini @@ -21,7 +21,7 @@ COM_USERS_EMAIL_REGISTERED_WITH_ACTIVATION_BODY="Hello {NAME},\n\nThank you for COM_USERS_EMAIL_REGISTERED_WITH_ACTIVATION_BODY_NOPW="Hello {NAME},\n\nThank you for registering at {SITENAME}. Your account is created and must be activated before you can use it.\nTo activate the account select the following link or copy-paste it in your browser:\n{ACTIVATE} \n\nAfter activation you may login to {SITEURL} using the following username and the password you entered during registration:\n\nUsername: {USERNAME}" COM_USERS_EMAIL_REGISTERED_WITH_ADMIN_ACTIVATION_BODY="Hello {NAME},\n\nThank you for registering at {SITENAME}. Your account is created and must be verified before you can use it.\n\nTo verify the account select the following link or copy-paste it in your browser:\n{ACTIVATE} \n\nAfter verification an administrator will be notified to activate your account. You'll receive a confirmation when it's done.\n\nOnce that account has been activated you may login to {SITEURL} using the following username and password:\n\nUsername: {USERNAME}\nPassword: {PASSWORD_CLEAR}" COM_USERS_EMAIL_REGISTERED_WITH_ADMIN_ACTIVATION_BODY_NOPW="Hello {NAME},\n\nThank you for registering at {SITENAME}. Your account is created and must be verified before you can use it.\n\nTo verify the account select the following link or copy-paste it in your browser:\n{ACTIVATE} \n\nAfter verification an administrator will be notified to activate your account. You'll receive a confirmation when it's done.\n\nOnce that account has been activated you may login to {SITEURL} using the following username and the password you entered during registration:\n\nUsername: {USERNAME}" -COM_USERS_EMAIL_USERNAME_REMINDER_BODY="Hello,\n\nA username reminder has been requested for your {SITENAME} account.\n\nYour username is {USERNAME}.\n\nTo login to your account, select the link below.\n\n{LINK_TEXT} \n\nThank you." +COM_USERS_EMAIL_USERNAME_REMINDER_BODY="Hello,\n\nA username reminder has been requested for your {SITENAME} account.\n\nYour username: {USERNAME}\n\nTo login to your account, select the link below.\n\n{LINK_TEXT} \n\nThank you." COM_USERS_EMAIL_USERNAME_REMINDER_SUBJECT="Your {SITENAME} username" COM_USERS_FIELD_PASSWORD_RESET_LABEL="Email Address" COM_USERS_FIELD_REMIND_EMAIL_LABEL="Email Address" diff --git a/language/en-GB/mod_breadcrumbs.ini b/language/en-GB/mod_breadcrumbs.ini index 1d7475000a8c3..3aa6fb573efc9 100644 --- a/language/en-GB/mod_breadcrumbs.ini +++ b/language/en-GB/mod_breadcrumbs.ini @@ -11,4 +11,4 @@ MOD_BREADCRUMBS_FIELD_SHOWHOME_LABEL="Home" MOD_BREADCRUMBS_FIELD_SHOWLAST_LABEL="Last" MOD_BREADCRUMBS_HERE="You are here: " MOD_BREADCRUMBS_HOME="Home" -MOD_BREADCRUMBS_XML_DESCRIPTION="This module displays the Breadcrumbs." \ No newline at end of file +MOD_BREADCRUMBS_XML_DESCRIPTION="This module displays the Breadcrumbs." diff --git a/libraries/src/Application/ConsoleApplication.php b/libraries/src/Application/ConsoleApplication.php index 9b2b4703356ef..ee6f69bb1f502 100644 --- a/libraries/src/Application/ConsoleApplication.php +++ b/libraries/src/Application/ConsoleApplication.php @@ -593,7 +593,7 @@ protected function getDefaultInputDefinition(): InputDefinition * * @return mixed The user state or null. * - * @since 5.0.0 + * @since 4.4.0 */ public function getUserState($key, $default = null) { @@ -616,7 +616,7 @@ public function getUserState($key, $default = null) * * @return mixed The request user state. * - * @since 5.0.0 + * @since 4.4.0 */ public function getUserStateFromRequest($key, $request, $default = null, $type = 'none') { diff --git a/libraries/src/Application/DaemonApplication.php b/libraries/src/Application/DaemonApplication.php index a9b06fcc403e0..1e08bd2375e76 100644 --- a/libraries/src/Application/DaemonApplication.php +++ b/libraries/src/Application/DaemonApplication.php @@ -134,7 +134,9 @@ public function __construct(Cli $input = null, Registry $config = null, Dispatch parent::__construct($input, $config, null, null, $dispatcher); // Set some system limits. - @set_time_limit($this->config->get('max_execution_time', 0)); + if (\function_exists('set_time_limit')) { + 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')); diff --git a/libraries/src/Console/CleanCacheCommand.php b/libraries/src/Console/CleanCacheCommand.php index 4eb43fa4734b5..ef8d41466024c 100644 --- a/libraries/src/Console/CleanCacheCommand.php +++ b/libraries/src/Console/CleanCacheCommand.php @@ -52,7 +52,7 @@ protected function doExecute(InputInterface $input, OutputInterface $output): in $symfonyStyle->title('Cleaning System Cache'); $cache = $this->getApplication()->bootComponent('com_cache')->getMVCFactory(); - /** @var Joomla\Component\Cache\Administrator\Model\CacheModel $model */ + /** @var \Joomla\Component\Cache\Administrator\Model\CacheModel $model */ $model = $cache->createModel('Cache', 'Administrator', ['ignore_request' => true]); if ($input->getArgument('expired')) { diff --git a/libraries/src/Console/FinderIndexCommand.php b/libraries/src/Console/FinderIndexCommand.php index f58d4560c7821..29db28b394bb5 100644 --- a/libraries/src/Console/FinderIndexCommand.php +++ b/libraries/src/Console/FinderIndexCommand.php @@ -366,7 +366,9 @@ private function index() $app->triggerEvent('onStartIndex'); // Remove the script time limit. - @set_time_limit(0); + if (\function_exists('set_time_limit')) { + set_time_limit(0); + } // Get the indexer state. $state = Indexer::getState(); diff --git a/libraries/src/Console/UpdateCoreCommand.php b/libraries/src/Console/UpdateCoreCommand.php index faae61e6748ab..72d000acbd280 100644 --- a/libraries/src/Console/UpdateCoreCommand.php +++ b/libraries/src/Console/UpdateCoreCommand.php @@ -406,7 +406,7 @@ public function copyFileTo($file, $dir): void * * @return integer the number of errors * - * @since 5.0.0 + * @since 4.4.0 */ public function checkSchema(): int { diff --git a/libraries/src/Filesystem/File.php b/libraries/src/Filesystem/File.php index d72f56901d04f..40bb99b29071d 100644 --- a/libraries/src/Filesystem/File.php +++ b/libraries/src/Filesystem/File.php @@ -407,7 +407,9 @@ public static function move($src, $dest, $path = '', $useStreams = false) */ public static function write($file, $buffer, $useStreams = false) { - @set_time_limit(ini_get('max_execution_time')); + if (\function_exists('set_time_limit')) { + set_time_limit(ini_get('max_execution_time')); + } // If the destination directory doesn't exist we need to create it if (!file_exists(\dirname($file))) { @@ -466,7 +468,9 @@ public static function write($file, $buffer, $useStreams = false) */ public static function append($file, $buffer, $useStreams = false) { - @set_time_limit(ini_get('max_execution_time')); + if (\function_exists('set_time_limit')) { + set_time_limit(ini_get('max_execution_time')); + } // If the file doesn't exist, just write instead of append if (!file_exists($file)) { diff --git a/libraries/src/Filesystem/Folder.php b/libraries/src/Filesystem/Folder.php index 82a8105685c4e..e126b3f14c3f9 100644 --- a/libraries/src/Filesystem/Folder.php +++ b/libraries/src/Filesystem/Folder.php @@ -46,7 +46,9 @@ abstract class Folder */ public static function copy($src, $dest, $path = '', $force = false, $useStreams = false) { - @set_time_limit(ini_get('max_execution_time')); + if (\function_exists('set_time_limit')) { + set_time_limit(ini_get('max_execution_time')); + } $FTPOptions = ClientHelper::getCredentials('ftp'); @@ -285,7 +287,9 @@ public static function create($path = '', $mode = 0755) */ public static function delete($path) { - @set_time_limit(ini_get('max_execution_time')); + if (\function_exists('set_time_limit')) { + set_time_limit(ini_get('max_execution_time')); + } // Sanity check if (!$path) { @@ -566,7 +570,9 @@ public static function folders( */ protected static function _items($path, $filter, $recurse, $full, $exclude, $excludeFilterString, $findFiles) { - @set_time_limit(ini_get('max_execution_time')); + if (\function_exists('set_time_limit')) { + set_time_limit(ini_get('max_execution_time')); + } $arr = []; diff --git a/libraries/src/Installer/InstallerHelper.php b/libraries/src/Installer/InstallerHelper.php index 5989e144ea250..e32d4da0156f2 100644 --- a/libraries/src/Installer/InstallerHelper.php +++ b/libraries/src/Installer/InstallerHelper.php @@ -136,7 +136,9 @@ public static function downloadPackage($url, $target = false) ini_set('track_errors', $track_errors); // Bump the max execution time because not using built in php zip libs are slow - @set_time_limit(ini_get('max_execution_time')); + if (\function_exists('set_time_limit')) { + set_time_limit(ini_get('max_execution_time')); + } // Return the name of the downloaded package return basename($target); diff --git a/libraries/src/Updater/Adapter/ExtensionAdapter.php b/libraries/src/Updater/Adapter/ExtensionAdapter.php index 2314254c1ebe8..f8e7f8549d138 100644 --- a/libraries/src/Updater/Adapter/ExtensionAdapter.php +++ b/libraries/src/Updater/Adapter/ExtensionAdapter.php @@ -303,7 +303,14 @@ public function findUpdate($options) if (isset($this->latest)) { if (isset($this->latest->client) && \strlen($this->latest->client)) { - $this->latest->client_id = ApplicationHelper::getClientInfo($this->latest->client, true)->id; + /** + * The client_id in the update XML manifest can be either an integer (backwards + * compatible with Joomla 1.6–3.10) or a string. Backwards compatibility with the + * integer key is provided as update servers with the legacy, numeric IDs cause PHP notices + * during update retrieval. The proper string key is one of 'site' or 'administrator'. + */ + $this->latest->client_id = is_numeric($this->latest->client) ? $this->latest->client + : ApplicationHelper::getClientInfo($this->latest->client, true)->id; unset($this->latest->client); } diff --git a/package-lock.json b/package-lock.json index d4811dd31e22c..18df8164df1d0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -101,8 +101,8 @@ "terser": "^5.19.3" }, "engines": { - "node": ">=16", - "npm": ">=8.5.5" + "node": ">=18", + "npm": ">=9.6.7" } }, "node_modules/@aashutoshrathi/word-wrap": { diff --git a/package.json b/package.json index 56a5423592703..7bf093087304c 100644 --- a/package.json +++ b/package.json @@ -7,10 +7,9 @@ "type": "git", "url": "https://github.com/joomla/joomla-cms.git" }, - "__engines_upgrades__": "Dockerfiles @ github.com/joomla-projects/docker-images need also upgrade", "engines": { - "node": ">=16", - "npm": ">=8.5.5" + "node": ">=18", + "npm": ">=9.6.7" }, "scripts": { "build:js": "node build/build.js --compile-js", diff --git a/plugins/system/debug/src/DataCollector/MemoryCollector.php b/plugins/system/debug/src/DataCollector/MemoryCollector.php new file mode 100644 index 0000000000000..6bac4d1644f63 --- /dev/null +++ b/plugins/system/debug/src/DataCollector/MemoryCollector.php @@ -0,0 +1,151 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\Plugin\System\Debug\DataCollector; + +use Joomla\Plugin\System\Debug\AbstractDataCollector; +use Joomla\Registry\Registry; + +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects + +/** + * Collects info about the request duration as well as providing + * a way to log duration of any operations + * + * @since __DEPLOY_VERSION__ + */ +class MemoryCollector extends AbstractDataCollector +{ + /** + * @var boolean + * @since __DEPLOY_VERSION__ + */ + protected $realUsage = false; + + /** + * @var float + * @since __DEPLOY_VERSION__ + */ + protected $peakUsage = 0; + + /** + * @param Registry $params Parameters. + * @param float $peakUsage + * @param boolean $realUsage + * + * @since __DEPLOY_VERSION__ + */ + public function __construct(Registry $params, $peakUsage = null, $realUsage = null) + { + parent::__construct($params); + + if ($peakUsage !== null) { + $this->peakUsage = $peakUsage; + } + + if ($realUsage !== null) { + $this->realUsage = $realUsage; + } + } + + /** + * Returns whether total allocated memory page size is used instead of actual used memory size + * by the application. See $real_usage parameter on memory_get_peak_usage for details. + * + * @return boolean + * + * @since __DEPLOY_VERSION__ + */ + public function getRealUsage() + { + return $this->realUsage; + } + + /** + * Sets whether total allocated memory page size is used instead of actual used memory size + * by the application. See $real_usage parameter on memory_get_peak_usage for details. + * + * @param boolean $realUsage + * + * @since __DEPLOY_VERSION__ + */ + public function setRealUsage($realUsage) + { + $this->realUsage = $realUsage; + } + + /** + * Returns the peak memory usage + * + * @return integer + * + * @since __DEPLOY_VERSION__ + */ + public function getPeakUsage() + { + return $this->peakUsage; + } + + /** + * Updates the peak memory usage value + * + * @since __DEPLOY_VERSION__ + */ + public function updatePeakUsage() + { + if ($this->peakUsage === null) { + $this->peakUsage = memory_get_peak_usage($this->realUsage); + } + } + + /** + * @return array + * + * @since __DEPLOY_VERSION__ + */ + public function collect() + { + $this->updatePeakUsage(); + + return [ + 'peak_usage' => $this->peakUsage, + 'peak_usage_str' => $this->getDataFormatter()->formatBytes($this->peakUsage, 3), + ]; + } + + /** + * @return string + * + * @since __DEPLOY_VERSION__ + */ + public function getName() + { + return 'memory'; + } + + /** + * @return array + * + * @since __DEPLOY_VERSION__ + */ + public function getWidgets() + { + return [ + 'memory' => [ + 'icon' => 'cogs', + 'tooltip' => 'Memory Usage', + 'map' => 'memory.peak_usage_str', + 'default' => "'0B'", + ], + ]; + } +} diff --git a/plugins/system/debug/src/DataCollector/ProfileCollector.php b/plugins/system/debug/src/DataCollector/ProfileCollector.php index 0c1de924b7ad4..381ae45ff2a81 100644 --- a/plugins/system/debug/src/DataCollector/ProfileCollector.php +++ b/plugins/system/debug/src/DataCollector/ProfileCollector.php @@ -84,8 +84,9 @@ public function __construct(Registry $params) * @param string|null $label Public name * @param string|null $collector The source of the collector * - * @since 4.0.0 * @return void + * + * @since 4.0.0 */ public function startMeasure($name, $label = null, $collector = null) { @@ -103,8 +104,9 @@ public function startMeasure($name, $label = null, $collector = null) * * @param string $name Group name. * - * @since 4.0.0 * @return bool + * + * @since 4.0.0 */ public function hasStartedMeasure($name): bool { @@ -117,9 +119,11 @@ public function hasStartedMeasure($name): bool * @param string $name Measurement name. * @param array $params Parameters * + * @return void + * * @since 4.0.0 + * * @throws DebugBarException - * @return void */ public function stopMeasure($name, array $params = []) { @@ -129,13 +133,7 @@ public function stopMeasure($name, array $params = []) throw new DebugBarException("Failed stopping measure '$name' because it hasn't been started"); } - $this->addMeasure( - $this->startedMeasures[$name]['label'], - $this->startedMeasures[$name]['start'], - $end, - $params, - $this->startedMeasures[$name]['collector'] - ); + $this->addMeasure($this->startedMeasures[$name]['label'], $this->startedMeasures[$name]['start'], $end, $params, $this->startedMeasures[$name]['collector']); unset($this->startedMeasures[$name]); } @@ -149,8 +147,9 @@ public function stopMeasure($name, array $params = []) * @param array $params Parameters. * @param string|null $collector A collector. * - * @since 4.0.0 * @return void + * + * @since 4.0.0 */ public function addMeasure($label, $start, $end, array $params = [], $collector = null) { @@ -174,8 +173,9 @@ public function addMeasure($label, $start, $end, array $params = [], $collector * @param \Closure $closure A closure. * @param string|null $collector A collector. * - * @since 4.0.0 * @return void + * + * @since 4.0.0 */ public function measure($label, \Closure $closure, $collector = null) { @@ -189,8 +189,9 @@ public function measure($label, \Closure $closure, $collector = null) /** * Returns an array of all measures * - * @since 4.0.0 * @return array + * + * @since 4.0.0 */ public function getMeasures(): array { @@ -200,8 +201,9 @@ public function getMeasures(): array /** * Returns the request start time * - * @since 4.0.0 * @return float + * + * @since 4.0.0 */ public function getRequestStartTime(): float { @@ -211,8 +213,9 @@ public function getRequestStartTime(): float /** * Returns the request end time * - * @since 4.0.0 * @return float + * + * @since 4.0.0 */ public function getRequestEndTime(): float { @@ -222,8 +225,9 @@ public function getRequestEndTime(): float /** * Returns the duration of a request * - * @since 4.0.0 * @return float + * + * @since 4.0.0 */ public function getRequestDuration(): float { @@ -234,15 +238,32 @@ public function getRequestDuration(): float return microtime(true) - $this->requestStartTime; } + /** + * Sets request end time. + * + * @param float $time Request end time. + * + * @return $this + * + * @since __DEPLOY_VERSION__ + */ + public function setRequestEndTime($time): self + { + $this->requestEndTime = $time; + + return $this; + } + /** * Called by the DebugBar when data needs to be collected * - * @since 4.0.0 * @return array Collected data + * + * @since 4.0.0 */ public function collect(): array { - $this->requestEndTime = microtime(true); + $this->requestEndTime = $this->requestEndTime ?? microtime(true); $start = $this->requestStartTime; @@ -284,8 +305,9 @@ function ($a, $b) { /** * Returns the unique name of the collector * - * @since 4.0.0 * @return string + * + * @since 4.0.0 */ public function getName(): string { @@ -296,8 +318,9 @@ public function getName(): string * Returns a hash where keys are control names and their values * an array of options as defined in {@see \DebugBar\JavascriptRenderer::addControl()} * - * @since 4.0.0 * @return array + * + * @since 4.0.0 */ public function getWidgets(): array { diff --git a/plugins/system/debug/src/Extension/Debug.php b/plugins/system/debug/src/Extension/Debug.php index f08d726c3ee73..92bc83549725a 100644 --- a/plugins/system/debug/src/Extension/Debug.php +++ b/plugins/system/debug/src/Extension/Debug.php @@ -10,7 +10,6 @@ namespace Joomla\Plugin\System\Debug\Extension; -use DebugBar\DataCollector\MemoryCollector; use DebugBar\DataCollector\MessagesCollector; use DebugBar\DebugBar; use DebugBar\OpenHandler; @@ -29,11 +28,13 @@ use Joomla\Database\DatabaseInterface; use Joomla\Database\Event\ConnectionEvent; use Joomla\Event\DispatcherInterface; +use Joomla\Event\Event; use Joomla\Event\SubscriberInterface; use Joomla\Plugin\System\Debug\DataCollector\InfoCollector; use Joomla\Plugin\System\Debug\DataCollector\LanguageErrorsCollector; use Joomla\Plugin\System\Debug\DataCollector\LanguageFilesCollector; use Joomla\Plugin\System\Debug\DataCollector\LanguageStringsCollector; +use Joomla\Plugin\System\Debug\DataCollector\MemoryCollector; use Joomla\Plugin\System\Debug\DataCollector\ProfileCollector; use Joomla\Plugin\System\Debug\DataCollector\QueryCollector; use Joomla\Plugin\System\Debug\DataCollector\RequestDataCollector; @@ -69,7 +70,7 @@ final class Debug extends CMSPlugin implements SubscriberInterface * @var boolean * @since 3.0 */ - private $debugLang = false; + private $debugLang; /** * Holds log entries handled by the plugin. @@ -79,14 +80,6 @@ final class Debug extends CMSPlugin implements SubscriberInterface */ private $logEntries = []; - /** - * Holds SHOW PROFILES of queries. - * - * @var array - * @since 3.1.2 - */ - private $sqlShowProfiles = []; - /** * Holds all SHOW PROFILE FOR QUERY n, indexed by n-1. * @@ -103,14 +96,6 @@ final class Debug extends CMSPlugin implements SubscriberInterface */ private $explains = []; - /** - * Holds total amount of executed queries. - * - * @var int - * @since 3.2 - */ - private $totalQueries = 0; - /** * @var DebugBar * @since 4.0.0 @@ -134,7 +119,7 @@ final class Debug extends CMSPlugin implements SubscriberInterface protected $isAjax = false; /** - * Whether displaing a logs is enabled + * Whether displaying a logs is enabled * * @var bool * @since 4.0.0 @@ -142,8 +127,14 @@ final class Debug extends CMSPlugin implements SubscriberInterface protected $showLogs = false; /** - * Returns an array of events this subscriber will listen to. + * The time spent in onAfterDisconnect() * + * @var float + * @since __DEPLOY_VERSION__ + */ + protected $timeInOnAfterDisconnect = 0; + + /** * @return array * * @since 4.1.3 @@ -151,12 +142,18 @@ final class Debug extends CMSPlugin implements SubscriberInterface public static function getSubscribedEvents(): array { return [ - 'onBeforeCompileHead' => 'onBeforeCompileHead', - 'onAjaxDebug' => 'onAjaxDebug', - 'onBeforeRespond' => 'onBeforeRespond', - 'onAfterRespond' => 'onAfterRespond', - ApplicationEvents::AFTER_RESPOND => 'onAfterRespond', - 'onAfterDisconnect' => 'onAfterDisconnect', + 'onBeforeCompileHead' => 'onBeforeCompileHead', + 'onAjaxDebug' => 'onAjaxDebug', + 'onBeforeRespond' => 'onBeforeRespond', + 'onAfterRespond' => [ + 'onAfterRespond', + PHP_INT_MIN, + ], + ApplicationEvents::AFTER_RESPOND => [ + 'onAfterRespond', + PHP_INT_MIN, + ], + 'onAfterDisconnect' => 'onAfterDisconnect', ]; } @@ -182,7 +179,7 @@ public function __construct(DispatcherInterface $dispatcher, array $config, CMSA return; } - $this->getApplication()->getConfig()->set('gzip', false); + $this->getApplication()->set('gzip', false); ob_start(); ob_implicit_flush(false); @@ -270,8 +267,11 @@ public function onBeforeCompileHead() */ public function onAfterRespond() { + $endTime = microtime(true) - $this->timeInOnAfterDisconnect; + $endMemory = memory_get_peak_usage(false); + // Do not collect data if debugging or language debug is not enabled. - if (!JDEBUG && !$this->debugLang || $this->isAjax) { + if ((!JDEBUG && !$this->debugLang) || $this->isAjax) { return; } @@ -288,7 +288,7 @@ public function onAfterRespond() if (JDEBUG) { if ($this->params->get('memory', 1)) { - $this->debugBar->addCollector(new MemoryCollector()); + $this->debugBar->addCollector(new MemoryCollector($this->params, $endMemory)); } if ($this->params->get('request', 1)) { @@ -300,10 +300,13 @@ public function onAfterRespond() } if ($this->params->get('profile', 1)) { - $this->debugBar->addCollector(new ProfileCollector($this->params)); + $this->debugBar->addCollector((new ProfileCollector($this->params))->setRequestEndTime($endTime)); } if ($this->params->get('queries', 1)) { + // Close session to collect possible session-related queries. + $this->getApplication()->getSession()->close(); + // Call $db->disconnect() here to trigger the onAfterDisconnect() method here in this class! $this->getDatabase()->disconnect(); $this->debugBar->addCollector(new QueryCollector($this->params, $this->queryMonitor, $this->sqlShowProfileEach, $this->explains)); @@ -401,7 +404,7 @@ public function onAjaxDebug(AjaxEvent $event) */ private function isAuthorisedDisplayDebug(): bool { - static $result = null; + static $result; if ($result !== null) { return $result; @@ -440,13 +443,13 @@ public function onAfterDisconnect(ConnectionEvent $event) return; } + $startTime = microtime(true); + $db = $event->getDriver(); // Remove the monitor to avoid monitoring the following queries $db->setMonitor(null); - $this->totalQueries = $db->getCount(); - if ($this->params->get('query_profiles') && $db->getServerType() === 'mysql') { try { // Check if profiling is enabled. @@ -456,13 +459,13 @@ public function onAfterDisconnect(ConnectionEvent $event) if ($hasProfiling) { // Run a SHOW PROFILE query. $db->setQuery('SHOW PROFILES'); - $this->sqlShowProfiles = $db->loadAssocList(); + $sqlShowProfiles = $db->loadAssocList(); - if ($this->sqlShowProfiles) { - foreach ($this->sqlShowProfiles as $qn) { + if ($sqlShowProfiles) { + foreach ($sqlShowProfiles as $qn) { // Run SHOW PROFILE FOR QUERY for each query where a profile is available (max 100). $db->setQuery('SHOW PROFILE FOR QUERY ' . (int) $qn['Query_ID']); - $this->sqlShowProfileEach[(int) ($qn['Query_ID'] - 1)] = $db->loadAssocList(); + $this->sqlShowProfileEach[$qn['Query_ID'] - 1] = $db->loadAssocList(); } } } else { @@ -503,6 +506,8 @@ public function onAfterDisconnect(ConnectionEvent $event) } } } + + $this->timeInOnAfterDisconnect = microtime(true) - $startTime; } /** @@ -530,18 +535,18 @@ public function logger(LogEntry $entry) /** * Collect log messages. * - * @return $this + * @return void * * @since 4.0.0 */ - private function collectLogs(): self + private function collectLogs() { $loggerOptions = ['group' => 'default']; $logger = new InMemoryLogger($loggerOptions); $logEntries = $logger->getCollectedEntries(); if (!$this->logEntries && !$logEntries) { - return $this; + return; } if ($this->logEntries) { @@ -569,6 +574,7 @@ private function collectLogs(): self $this->debugBar[$entry->category]->addMessage($entry->message); } break; + case 'deprecated': if (!$logDeprecated && !$logDeprecatedCore) { break; @@ -643,8 +649,6 @@ private function collectLogs(): self break; } } - - return $this; } /** diff --git a/plugins/system/schedulerunner/src/Extension/ScheduleRunner.php b/plugins/system/schedulerunner/src/Extension/ScheduleRunner.php index 5ad6b42a896e2..655d970f8ba92 100644 --- a/plugins/system/schedulerunner/src/Extension/ScheduleRunner.php +++ b/plugins/system/schedulerunner/src/Extension/ScheduleRunner.php @@ -19,6 +19,7 @@ use Joomla\CMS\Session\Session; use Joomla\CMS\Table\Extension; use Joomla\CMS\User\UserHelper; +use Joomla\Component\Scheduler\Administrator\Model\TasksModel; use Joomla\Component\Scheduler\Administrator\Scheduler\Scheduler; use Joomla\Component\Scheduler\Administrator\Task\Task; use Joomla\Event\Event; @@ -109,22 +110,13 @@ public function injectLazyJS(EventInterface $event): void return; } - // Check if any task is due to decrease the load + /** @var TasksModel $model */ $model = $this->getApplication()->bootComponent('com_scheduler') ->getMVCFactory()->createModel('Tasks', 'Administrator', ['ignore_request' => true]); - $model->setState('filter.state', 1); - $model->setState('filter.due', 1); + $now = Factory::getDate('now', 'UTC'); - $items = $model->getItems(); - - // See if we are running currently - $model->setState('filter.locked', 1); - $model->setState('filter.due', 0); - - $items2 = $model->getItems(); - - if (empty($items) || !empty($items2)) { + if (!$model->hasDueTasks($now)) { return; } @@ -262,7 +254,7 @@ public function runTestCron(Event $event) ] ); - if (!is_null($task)) { + if ($task) { $task->run(); $event->addArgument('result', $task->getContent()); } else { @@ -286,7 +278,7 @@ public function runTestCron(Event $event) * @return ?Task * * @since 4.1.0 - * @throws RuntimeException + * @throws \RuntimeException */ private function runScheduler(int $id = 0): ?Task { diff --git a/tests/System/integration/api/com_modules/Administrator.cy.js b/tests/System/integration/api/com_modules/Administrator.cy.js new file mode 100644 index 0000000000000..82f494a18c405 --- /dev/null +++ b/tests/System/integration/api/com_modules/Administrator.cy.js @@ -0,0 +1,75 @@ +describe('Test that modules administrator API endpoint', () => { + afterEach(() => cy.task('queryDB', "DELETE FROM #__modules WHERE title = 'automated test administrator module'")); + + it('can deliver a list of administrator modules', () => { + cy.api_get('/modules/administrator') + .then((response) => cy.wrap(response).its('body').its('data.0').its('attributes') + .its('module') + .should('include', 'mod_sampledata')); + }); + + it('can deliver a single administrator module', () => { + cy.db_createModule({ title: 'automated test administrator module', client_id: 1 }) + .then((module) => cy.api_get(`/modules/administrator/${module}`)) + .then((response) => cy.wrap(response).its('body').its('data').its('attributes') + .its('title') + .should('include', 'automated test administrator module')); + }); + + it('can create an administrator module', () => { + cy.api_post('/modules/administrator', { + access: '1', + assigned: [ + '101', + '105', + ], + assignment: '0', + client_id: '1', + language: '0', + module: 'mod_version', + note: '', + ordering: '1', + params: { + bootstrap_size: '0', + cache: '1', + cache_time: '900', + cachemode: 'static', + count: '10', + header_class: '', + header_tag: 'h3', + layout: '_:default', + module_tag: 'div', + moduleclass_sfx: '', + style: '0', + }, + position: '', + publish_down: '', + publish_up: '', + published: '1', + showtitle: '1', + title: 'automated test administrator module', + }) + .then((response) => cy.wrap(response).its('body').its('data').its('attributes') + .its('title') + .should('include', 'automated test administrator module')); + }); + + it('can update an administrator module', () => { + cy.db_createModule({ title: 'automated test administrator module', client_id: 1 }) + .then((id) => { + const updatedModuleData = { + published: -2, + }; + return cy.api_patch(`/modules/administrator/${id}`, updatedModuleData); + }) + .then((response) => cy.wrap(response).its('body').its('data').its('attributes') + .its('published') + .should('equal', -2)); + }); + + it('can delete a administrator module', () => { + cy.db_createModule({ title: 'automated test administrator module', published: -2, client_id: 1 }) + .then((module) => cy.api_delete(`/modules/administrator/${module}`)) + .then((response) => cy.wrap(response).its('status').should('equal', 204)); + }); +}); diff --git a/tests/System/integration/api/com_modules/Site.cy.js b/tests/System/integration/api/com_modules/Site.cy.js new file mode 100644 index 0000000000000..0aee39c1ea552 --- /dev/null +++ b/tests/System/integration/api/com_modules/Site.cy.js @@ -0,0 +1,75 @@ +describe('Test that modules site API endpoint', () => { + afterEach(() => cy.task('queryDB', "DELETE FROM #__modules WHERE title = 'automated test site module'")); + + it('can deliver a list of site modules', () => { + cy.api_get('/modules/site') + .then((response) => cy.wrap(response).its('body').its('data.0').its('attributes') + .its('module') + .should('include', 'mod_breadcrumbs')); + }); + + it('can deliver a single site module', () => { + cy.db_createModule({ title: 'automated test site module' }) + .then((module) => cy.api_get(`/modules/site/${module}`)) + .then((response) => cy.wrap(response).its('body').its('data').its('attributes') + .its('title') + .should('include', 'automated test site module')); + }); + + it('can create a site module', () => { + cy.api_post('/modules/site', { + access: '1', + assigned: [ + '101', + '105', + ], + assignment: '0', + client_id: '0', + language: '0', + module: 'mod_articles_archive', + note: '', + ordering: '1', + params: { + bootstrap_size: '0', + cache: '1', + cache_time: '900', + cachemode: 'static', + count: '10', + header_class: '', + header_tag: 'h3', + layout: '_:default', + module_tag: 'div', + moduleclass_sfx: '', + style: '0', + }, + position: '', + publish_down: '', + publish_up: '', + published: '1', + showtitle: '1', + title: 'automated test site module', + }) + .then((response) => cy.wrap(response).its('body').its('data').its('attributes') + .its('title') + .should('include', 'automated test site module')); + }); + + it('can update a site module', () => { + cy.db_createModule({ title: 'automated test site module' }) + .then((id) => { + const updatedModuleData = { + published: -2, + }; + return cy.api_patch(`/modules/site/${id}`, updatedModuleData); + }) + .then((response) => cy.wrap(response).its('body').its('data').its('attributes') + .its('published') + .should('equal', -2)); + }); + + it('can delete a site module', () => { + cy.db_createModule({ title: 'automated test site module', published: -2 }) + .then((module) => cy.api_delete(`/modules/site/${module}`)) + .then((response) => cy.wrap(response).its('status').should('equal', 204)); + }); +}); diff --git a/tests/System/integration/api/com_modules/Types.cy.js b/tests/System/integration/api/com_modules/Types.cy.js new file mode 100644 index 0000000000000..219fb4bae8f5c --- /dev/null +++ b/tests/System/integration/api/com_modules/Types.cy.js @@ -0,0 +1,15 @@ +describe('Test that modules types API endpoint', () => { + it('can deliver a list of modules types administrator', () => { + cy.api_get('/modules/types/administrator') + .then((response) => cy.wrap(response).its('body').its('data.0').its('attributes') + .its('module') + .should('include', 'mod_latestactions')); + }); + + it('can deliver a list of modules types site', () => { + cy.api_get('/modules/types/site') + .then((response) => cy.wrap(response).its('body').its('data.0').its('attributes') + .its('module') + .should('include', 'mod_articles_archive')); + }); +}); diff --git a/tests/System/integration/api/com_plugins/Plugins.cy.js b/tests/System/integration/api/com_plugins/Plugins.cy.js new file mode 100644 index 0000000000000..5e6ccaddbaf26 --- /dev/null +++ b/tests/System/integration/api/com_plugins/Plugins.cy.js @@ -0,0 +1,35 @@ +describe('Test that plugins API endpoint', () => { + it('can deliver a list of plugins', () => { + cy.api_get('/plugins') + .then((response) => cy.wrap(response).its('body').its('data.0').its('attributes') + .its('folder') + .should('include', 'actionlog')); + }); + + it('can deliver a single plugin', () => { + cy.api_get('/plugins') + .then((response) => cy.wrap(response).its('body').its('data.0').its('attributes') + .its('id')) + .then((id) => { + cy.api_get(`/plugins/${id}`) + .then((response) => cy.wrap(response).its('body').its('data').its('attributes') + .its('folder') + .should('include', 'actionlog')); + }); + }); + + it('can modify a single plugin', () => { + cy.api_get('/plugins') + .then((response) => cy.wrap(response).its('body').its('data.0').its('attributes') + .its('id')) + .then((id) => { + const updatedPlugin = { + enabled: 0, + }; + cy.api_patch(`/plugins/${id}`, updatedPlugin) + .then((response) => cy.wrap(response).its('body').its('data').its('attributes') + .its('enabled') + .should('equal', 0)); + }); + }); +}); diff --git a/tests/System/integration/api/com_templates/Administrator.cy.js b/tests/System/integration/api/com_templates/Administrator.cy.js new file mode 100644 index 0000000000000..6013b793cf85b --- /dev/null +++ b/tests/System/integration/api/com_templates/Administrator.cy.js @@ -0,0 +1,35 @@ +describe('Test that templates administrator styles API endpoint', () => { + it('can deliver a list of templates administrator styles', () => { + cy.api_get('/templates/styles/administrator') + .then((response) => cy.wrap(response).its('body').its('data.0').its('attributes') + .its('template') + .should('include', 'atum')); + }); + + it('can deliver a single templates administrator style', () => { + cy.api_get('/templates/styles/administrator') + .then((response) => cy.wrap(response).its('body').its('data.0').its('attributes') + .its('id')) + .then((id) => { + cy.api_get(`/templates/styles/administrator/${id}`) + .then((response) => cy.wrap(response).its('body').its('data').its('attributes') + .its('template') + .should('include', 'atum')); + }); + }); + + it('can modify a single template administrator style', () => { + cy.api_get('/templates/styles/administrator') + .then((response) => cy.wrap(response).its('body').its('data.0').its('attributes') + .its('id')) + .then((id) => { + const updatedStyle = { + title: 'automated test template administrator style', + }; + cy.api_patch(`/templates/styles/administrator/${id}`, updatedStyle) + .then((response) => cy.wrap(response).its('body').its('data').its('attributes') + .its('title') + .should('equal', 'automated test template administrator style')); + }); + }); +}); diff --git a/tests/System/integration/api/com_templates/Site.cy.js b/tests/System/integration/api/com_templates/Site.cy.js new file mode 100644 index 0000000000000..1b3faa73acd83 --- /dev/null +++ b/tests/System/integration/api/com_templates/Site.cy.js @@ -0,0 +1,35 @@ +describe('Test that templates API endpoint', () => { + it('can deliver a list of templates', () => { + cy.api_get('/templates/styles/site') + .then((response) => cy.wrap(response).its('body').its('data.0').its('attributes') + .its('template') + .should('include', 'cassiopeia')); + }); + + it('can deliver a single template', () => { + cy.api_get('/templates/styles/site') + .then((response) => cy.wrap(response).its('body').its('data.0').its('attributes') + .its('id')) + .then((id) => { + cy.api_get(`/templates/styles/site/${id}`) + .then((response) => cy.wrap(response).its('body').its('data').its('attributes') + .its('template') + .should('include', 'cassiopeia')); + }); + }); + + it('can modify a single template', () => { + cy.api_get('/templates/styles/site') + .then((response) => cy.wrap(response).its('body').its('data.0').its('attributes') + .its('id')) + .then((id) => { + const updatedStyle = { + title: 'automated test template site style', + }; + cy.api_patch(`/templates/styles/site/${id}`, updatedStyle) + .then((response) => cy.wrap(response).its('body').its('data').its('attributes') + .its('title') + .should('equal', 'automated test template site style')); + }); + }); +});