From 2e78f24aee5312f37532d0dfbcbb6d5c4b592ac6 Mon Sep 17 00:00:00 2001 From: Brian Teeman Date: Sat, 29 May 2021 07:44:56 +0100 Subject: [PATCH 01/57] [4.0] Notification icons (#34226) --- installation/sql/mysql/base.sql | 10 +++++----- installation/sql/postgresql/base.sql | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/installation/sql/mysql/base.sql b/installation/sql/mysql/base.sql index 6ee73edc19552..ce94a76d49d2e 100644 --- a/installation/sql/mysql/base.sql +++ b/installation/sql/mysql/base.sql @@ -311,12 +311,12 @@ INSERT INTO `#__extensions` (`package_id`, `name`, `type`, `element`, `folder`, (0, 'plg_privacy_content', 'plugin', 'content', 'privacy', 0, 1, 1, 0, 1, '', '{}', '', 4, 0), (0, 'plg_privacy_message', 'plugin', 'message', 'privacy', 0, 1, 1, 0, 1, '', '{}', '', 5, 0), (0, 'plg_privacy_user', 'plugin', 'user', 'privacy', 0, 1, 1, 0, 1, '', '{}', '', 6, 0), -(0, 'plg_quickicon_downloadkey', 'plugin', 'downloadkey', 'quickicon', 0, 1, 1, 0, 1, '', '', '', 1, 0), +(0, 'plg_quickicon_joomlaupdate', 'plugin', 'joomlaupdate', 'quickicon', 0, 1, 1, 0, 1, '', '', '', 1, 0), (0, 'plg_quickicon_extensionupdate', 'plugin', 'extensionupdate', 'quickicon', 0, 1, 1, 0, 1, '', '', '', 2, 0), -(0, 'plg_quickicon_joomlaupdate', 'plugin', 'joomlaupdate', 'quickicon', 0, 1, 1, 0, 1, '', '', '', 3, 0), -(0, 'plg_quickicon_overridecheck', 'plugin', 'overridecheck', 'quickicon', 0, 1, 1, 0, 1, '', '', '', 4, 0), -(0, 'plg_quickicon_phpversioncheck', 'plugin', 'phpversioncheck', 'quickicon', 0, 1, 1, 0, 1, '', '', '', 5, 0), -(0, 'plg_quickicon_privacycheck', 'plugin', 'privacycheck', 'quickicon', 0, 1, 1, 0, 1, '', '{}', '', 6, 0), +(0, 'plg_quickicon_overridecheck', 'plugin', 'overridecheck', 'quickicon', 0, 1, 1, 0, 1, '', '', '', 3, 0), +(0, 'plg_quickicon_downloadkey', 'plugin', 'downloadkey', 'quickicon', 0, 1, 1, 0, 1, '', '', '', 4, 0), +(0, 'plg_quickicon_privacycheck', 'plugin', 'privacycheck', 'quickicon', 0, 1, 1, 0, 1, '', '{}', '', 5, 0), +(0, 'plg_quickicon_phpversioncheck', 'plugin', 'phpversioncheck', 'quickicon', 0, 1, 1, 0, 1, '', '', '', 6, 0), (0, 'plg_sampledata_blog', 'plugin', 'blog', 'sampledata', 0, 1, 1, 0, 1, '', '', '', 1, 0), (0, 'plg_sampledata_multilang', 'plugin', 'multilang', 'sampledata', 0, 1, 1, 0, 1, '', '', '', 2, 0), (0, 'plg_system_accessibility', 'plugin', 'accessibility', 'system', 0, 0, 1, 0, 1, '', '{}', '', 1, 0), diff --git a/installation/sql/postgresql/base.sql b/installation/sql/postgresql/base.sql index 088c70c4506ab..0626d992cbd57 100644 --- a/installation/sql/postgresql/base.sql +++ b/installation/sql/postgresql/base.sql @@ -317,12 +317,12 @@ INSERT INTO "#__extensions" ("package_id", "name", "type", "element", "folder", (0, 'plg_privacy_content', 'plugin', 'content', 'privacy', 0, 1, 1, 0, 1, '', '{}', '', 4, 0), (0, 'plg_privacy_message', 'plugin', 'message', 'privacy', 0, 1, 1, 0, 1, '', '{}', '', 5, 0), (0, 'plg_privacy_user', 'plugin', 'user', 'privacy', 0, 1, 1, 0, 1, '', '{}', '', 6, 0), -(0, 'plg_quickicon_downloadkey', 'plugin', 'downloadkey', 'quickicon', 0, 1, 1, 0, 1, '', '', '', 1, 0), +(0, 'plg_quickicon_joomlaupdate', 'plugin', 'joomlaupdate', 'quickicon', 0, 1, 1, 0, 1, '', '', '', 1, 0), (0, 'plg_quickicon_extensionupdate', 'plugin', 'extensionupdate', 'quickicon', 0, 1, 1, 0, 1, '', '', '', 2, 0), -(0, 'plg_quickicon_joomlaupdate', 'plugin', 'joomlaupdate', 'quickicon', 0, 1, 1, 0, 1, '', '', '', 3, 0), -(0, 'plg_quickicon_overridecheck', 'plugin', 'overridecheck', 'quickicon', 0, 1, 1, 0, 1, '', '', '', 4, 0), -(0, 'plg_quickicon_phpversioncheck', 'plugin', 'phpversioncheck', 'quickicon', 0, 1, 1, 0, 1, '', '', '', 5, 0), -(0, 'plg_quickicon_privacycheck', 'plugin', 'privacycheck', 'quickicon', 0, 1, 1, 0, 1, '', '{}', '', 6, 0), +(0, 'plg_quickicon_overridecheck', 'plugin', 'overridecheck', 'quickicon', 0, 1, 1, 0, 1, '', '', '', 3, 0), +(0, 'plg_quickicon_downloadkey', 'plugin', 'downloadkey', 'quickicon', 0, 1, 1, 0, 1, '', '', '', 4, 0), +(0, 'plg_quickicon_privacycheck', 'plugin', 'privacycheck', 'quickicon', 0, 1, 1, 0, 1, '', '{}', '', 5, 0), +(0, 'plg_quickicon_phpversioncheck', 'plugin', 'phpversioncheck', 'quickicon', 0, 1, 1, 0, 1, '', '', '', 6, 0), (0, 'plg_sampledata_blog', 'plugin', 'blog', 'sampledata', 0, 1, 1, 0, 1, '', '', '', 1, 0), (0, 'plg_sampledata_multilang', 'plugin', 'multilang', 'sampledata', 0, 1, 1, 0, 1, '', '', '', 2, 0), (0, 'plg_system_accessibility', 'plugin', 'accessibility', 'system', 0, 0, 1, 0, 1, '', '{}', '', 1, 0), From dbdf8aa27f344c3d14dfa39b138a6cede385198f Mon Sep 17 00:00:00 2001 From: "Phil E. Taylor" Date: Sat, 29 May 2021 07:52:45 +0100 Subject: [PATCH 02/57] remove method allow parent to run (#34118) Co-authored-by: Richard Fath --- .../com_content/src/Model/ArchiveModel.php | 35 ++++++++++--------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/components/com_content/src/Model/ArchiveModel.php b/components/com_content/src/Model/ArchiveModel.php index b4a707e75c85e..50f236369c069 100644 --- a/components/com_content/src/Model/ArchiveModel.php +++ b/components/com_content/src/Model/ArchiveModel.php @@ -130,30 +130,31 @@ protected function getListQuery() } /** - * Model override to add alternating value for $odd + * Method to get the archived article list * - * @param string $query The query. - * @param integer $limitstart Offset. - * @param integer $limit The number of records. - * - * @return array An array of results. - * - * @since 3.0.1 - * @throws \RuntimeException + * @access public + * @return array */ - protected function _getList($query, $limitstart=0, $limit=0) + public function getData() { - $result = parent::_getList($query, $limitstart, $limit); - - $odd = 1; + $app = Factory::getApplication(); - foreach ($result as $k => $row) + // Lets load the content if it doesn't already exist + if (empty($this->_data)) { - $result[$k]->odd = $odd; - $odd = 1 - $odd; + // 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 $result; + return $this->_data; } /** From aba836041032237dd9f1862c343163f5e0749a7f Mon Sep 17 00:00:00 2001 From: infograf768 Date: Sat, 29 May 2021 09:06:57 +0200 Subject: [PATCH 03/57] [4.0] Changing title to tooltip for template preview (#33292) * [4.0] Changing title to tooltip fpr template preview --- .../components/com_templates/tmpl/styles/default.php | 10 ++++------ .../com_templates/tmpl/templates/default.php | 7 +++---- administrator/language/en-GB/com_templates.ini | 1 + 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/administrator/components/com_templates/tmpl/styles/default.php b/administrator/components/com_templates/tmpl/styles/default.php index 4f9096696ce5f..9039e612b56d6 100644 --- a/administrator/components/com_templates/tmpl/styles/default.php +++ b/administrator/components/com_templates/tmpl/styles/default.php @@ -84,13 +84,11 @@ preview) : ?> client_id === 1 ? 'administrator' : 'site'; ?> - - - + + - - + + diff --git a/administrator/components/com_templates/tmpl/templates/default.php b/administrator/components/com_templates/tmpl/templates/default.php index 988075853eda6..43a4a2b727b3a 100644 --- a/administrator/components/com_templates/tmpl/templates/default.php +++ b/administrator/components/com_templates/tmpl/templates/default.php @@ -71,11 +71,10 @@
preview) : ?> client_id === 1 ? 'administrator' : 'site'; ?> - - + + + diff --git a/administrator/language/en-GB/com_templates.ini b/administrator/language/en-GB/com_templates.ini index 462797ea1315e..bc118323c0d34 100644 --- a/administrator/language/en-GB/com_templates.ini +++ b/administrator/language/en-GB/com_templates.ini @@ -224,6 +224,7 @@ COM_TEMPLATES_TEMPLATE_FILENAME="Editing file "%s" in template "% COM_TEMPLATES_TEMPLATE_FILES="Template Files" COM_TEMPLATES_TEMPLATE_NEW_NAME_DESC="Letters, numbers and underscore only." COM_TEMPLATES_TEMPLATE_NEW_NAME_LABEL="New Template Name" +COM_TEMPLATES_TEMPLATE_NEW_PREVIEW="Preview %s in new window." COM_TEMPLATES_TEMPLATE_NO_PREVIEW="No preview available. You can enable preview in the options." COM_TEMPLATES_TEMPLATE_NOT_SPECIFIED="Template not specified." COM_TEMPLATES_TEMPLATE_PREVIEW="Preview" From 1eab8c0ea8f266d2701093dac3e5a4460143dcf0 Mon Sep 17 00:00:00 2001 From: Quy Date: Sat, 29 May 2021 00:56:52 -0700 Subject: [PATCH 04/57] Update location path in description (#34238) --- administrator/language/en-GB/com_modules.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/administrator/language/en-GB/com_modules.ini b/administrator/language/en-GB/com_modules.ini index 3b0c7e9659e74..7215fb22edee7 100644 --- a/administrator/language/en-GB/com_modules.ini +++ b/administrator/language/en-GB/com_modules.ini @@ -94,7 +94,7 @@ COM_MODULES_MODULE_TEMPLATE_POSITION="%1$s (%2$s)" COM_MODULES_MODULES="Modules" COM_MODULES_MODULES_FILTER_SEARCH_DESC="Search in module title and note. Prefix with ID: to search for a module ID." COM_MODULES_MODULES_FILTER_SEARCH_LABEL="Search Modules" -COM_MODULES_MSG_MANAGE_EXTENSION_DISABLED="The '%s' module is disabled. Use Extensions => Manage to enable it." +COM_MODULES_MSG_MANAGE_EXTENSION_DISABLED="The '%s' module is disabled. Use System > Manage > Extensions to enable it." COM_MODULES_MSG_MANAGE_NO_MODULES="There are no modules matching your query" COM_MODULES_N_ITEMS_ARCHIVED="%d modules archived." COM_MODULES_N_ITEMS_ARCHIVED_1="Module archived." From d560b16ebeae54ac9e2f7c9ca94b4011de1a4900 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sat, 29 May 2021 10:00:17 +0200 Subject: [PATCH 05/57] Fix patterns field check when field empty and not required (#34124) --- build/media_source/system/js/fields/validate.es6.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/media_source/system/js/fields/validate.es6.js b/build/media_source/system/js/fields/validate.es6.js index 4b5c9810b0964..b5b6f1e053aeb 100644 --- a/build/media_source/system/js/fields/validate.es6.js +++ b/build/media_source/system/js/fields/validate.es6.js @@ -223,7 +223,7 @@ class JFormValidator { } this.handleResponse(true, element); - return false; + return true; } if (handler === '') { From d240975ae87887a51fdbba700abac0fd7a1161c6 Mon Sep 17 00:00:00 2001 From: Richard Fath Date: Sat, 29 May 2021 10:02:24 +0200 Subject: [PATCH 06/57] [4.0] Finish transition from CSS classes "label-" to "alert-" for the pre-update check (#34227) --- .../joomlaupdate/default_preupdatecheck.php | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/default_preupdatecheck.php b/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/default_preupdatecheck.php index 32cb7d93ffa1d..43466aeccb56b 100644 --- a/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/default_preupdatecheck.php +++ b/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/default_preupdatecheck.php @@ -28,7 +28,7 @@ 'group' => 0, ), 'COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSIONS_PRE_UPDATE_CHECKS_FAILED' => array( - 'class' => 'label-important', + 'class' => 'alert-danger', 'notes' => 'COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSIONS_PRE_UPDATE_CHECKS_FAILED_NOTES', 'group' => 4, ), @@ -65,15 +65,15 @@ foreach ($this->phpOptions as $option) : if (!$option->state) { - $labelClass = 'important'; + $labelClass = 'danger'; break; } endforeach; ?> - -

+ +

'); ?>

@@ -130,8 +130,8 @@ endforeach; ?> - -

+ +

@@ -189,8 +189,8 @@
- -

+ +

'); ?>
From 810b2ef0fe41455ac24c325799ba60f00a863f22 Mon Sep 17 00:00:00 2001 From: Brian Teeman Date: Sat, 29 May 2021 18:00:33 +0100 Subject: [PATCH 07/57] [4.0] mod_popular with disabled hits (#34257) * [4.0] mod_popular with disabled hits When hits are disabled for articles it makes no sense to display the Most Read Articles module in the site or admin as the contents will never be updated. This PR changes the output of the module so that instead of a list of non-updating articles a message is displayed. --- administrator/language/en-GB/joomla.ini | 1 + administrator/modules/mod_popular/mod_popular.php | 11 ++++++++++- api/language/en-GB/joomla.ini | 1 + language/en-GB/joomla.ini | 1 + layouts/joomla/content/emptystate_module.php | 4 ++-- .../src/Helper/ArticlesPopularHelper.php | 9 +++++++++ 6 files changed, 24 insertions(+), 3 deletions(-) diff --git a/administrator/language/en-GB/joomla.ini b/administrator/language/en-GB/joomla.ini index fdde6d36301ec..128ec950ee8d6 100644 --- a/administrator/language/en-GB/joomla.ini +++ b/administrator/language/en-GB/joomla.ini @@ -563,6 +563,7 @@ JGLOBAL_RANDOM_ORDER="Random Order" JGLOBAL_RATINGS="Ratings" JGLOBAL_RATINGS_ASC="Ratings ascending" JGLOBAL_RATINGS_DESC="Ratings descending" +JGLOBAL_RECORD_HITS_DISABLED="The recording of hits is disabled." JGLOBAL_RECORD_HITS_LABEL="Record Hits" JGLOBAL_RECORD_NUMBER="Record ID: %d" JGLOBAL_REMEMBER_ME="Remember Me" diff --git a/administrator/modules/mod_popular/mod_popular.php b/administrator/modules/mod_popular/mod_popular.php index 0c9b4b2b98470..c8bc50e6fe3cb 100644 --- a/administrator/modules/mod_popular/mod_popular.php +++ b/administrator/modules/mod_popular/mod_popular.php @@ -9,6 +9,7 @@ defined('_JEXEC') or die; +use Joomla\CMS\Component\ComponentHelper; use Joomla\CMS\Helper\ModuleHelper; use Joomla\CMS\Layout\LayoutHelper; use Joomla\Module\Popular\Administrator\Helper\PopularHelper; @@ -22,7 +23,15 @@ $module->title = PopularHelper::getTitle($params); } -if (count($list)) +if (!ComponentHelper::getParams('com_content')->get('record_hits')) +{ + echo LayoutHelper::render('joomla.content.emptystate_module', [ + 'title' => 'JGLOBAL_RECORD_HITS_DISABLED', + 'icon' => 'icon-minus-circle', + ] + ); +} +elseif (count($list)) { require ModuleHelper::getLayoutPath('mod_popular', $params->get('layout', 'default')); } diff --git a/api/language/en-GB/joomla.ini b/api/language/en-GB/joomla.ini index 4ab6b32a08a9d..cb1b3594c42e9 100644 --- a/api/language/en-GB/joomla.ini +++ b/api/language/en-GB/joomla.ini @@ -558,6 +558,7 @@ JGLOBAL_RANDOM_ORDER="Random Order" JGLOBAL_RATINGS="Ratings" JGLOBAL_RATINGS_ASC="Ratings ascending" JGLOBAL_RATINGS_DESC="Ratings descending" +JGLOBAL_RECORD_HITS_DISABLED="The recording of hits is disabled." JGLOBAL_RECORD_HITS_LABEL="Record Hits" JGLOBAL_RECORD_NUMBER="Record ID: %d" JGLOBAL_REMEMBER_ME="Remember Me" diff --git a/language/en-GB/joomla.ini b/language/en-GB/joomla.ini index eafdcdd861d1e..d8053f4b4c042 100644 --- a/language/en-GB/joomla.ini +++ b/language/en-GB/joomla.ini @@ -326,6 +326,7 @@ JGLOBAL_PREVIEW_POSITION="Position: %s" JGLOBAL_PREVIEW_STYLE="Style: %s" JGLOBAL_PRINT="Print" JGLOBAL_PRINT_TITLE="Print article < %s >" +JGLOBAL_RECORD_HITS_DISABLED="The recording of hits is disabled." JGLOBAL_RECORD_NUMBER="Record ID: %d" JGLOBAL_REMEMBER_ME="Remember me" JGLOBAL_REMEMBER_MUST_LOGIN="For security reasons you must login before editing your personal information." diff --git a/layouts/joomla/content/emptystate_module.php b/layouts/joomla/content/emptystate_module.php index d7653c4454203..a8331521174b2 100644 --- a/layouts/joomla/content/emptystate_module.php +++ b/layouts/joomla/content/emptystate_module.php @@ -12,7 +12,7 @@ use Joomla\CMS\Factory; use Joomla\CMS\Language\Text; -$textPrefix = $displayData['textPrefix']; +$textPrefix = $displayData['textPrefix'] ?? ''; $icon = $displayData['icon'] ?? 'icon-copy article'; $componentLangString = $textPrefix . '_EMPTYSTATE_TITLE'; $moduleLangString = $textPrefix . '_EMPTYSTATE_MODULE_TITLE' . (array_key_exists('textSuffix', $displayData) ? $displayData['textSuffix'] : ''); @@ -20,7 +20,7 @@ // Did we have a definitive title provided to the view? if (isset($displayData['title'])) { - $title = $displayData['title']; + $title = Text::_($displayData['title']); } // Can we find a *_EMPTYSTATE_MODULE_TITLE translation? elseif (Factory::getApplication()->getLanguage()->hasKey($moduleLangString)) diff --git a/modules/mod_articles_popular/src/Helper/ArticlesPopularHelper.php b/modules/mod_articles_popular/src/Helper/ArticlesPopularHelper.php index 3dff24350b1cf..c91657ee0ed51 100644 --- a/modules/mod_articles_popular/src/Helper/ArticlesPopularHelper.php +++ b/modules/mod_articles_popular/src/Helper/ArticlesPopularHelper.php @@ -14,6 +14,7 @@ use Joomla\CMS\Access\Access; use Joomla\CMS\Component\ComponentHelper; use Joomla\CMS\Factory; +use Joomla\CMS\Language\Text; use Joomla\CMS\Router\Route; use Joomla\Component\Content\Administrator\Extension\ContentComponent; use Joomla\Component\Content\Site\Helper\RouteHelper; @@ -36,6 +37,14 @@ public static function getList(&$params) { $app = Factory::getApplication(); + // Exit early if hits are disabled + if (!ComponentHelper::getParams('com_content')->get('record_hits')) + { + echo Text::_('JGLOBAL_RECORD_HITS_DISABLED'); + + return; + } + // Get an instance of the generic articles model $model = $app->bootComponent('com_content') ->getMVCFactory()->createModel('Articles', 'Site', ['ignore_request' => true]); From c9e8d9342a8b7c264968e2ecb28cb738a0e78ce3 Mon Sep 17 00:00:00 2001 From: Tuan Pham Ngoc Date: Sun, 30 May 2021 08:31:23 +0700 Subject: [PATCH 08/57] [4.0] Use MVCFactory to create model (#34092) * Use MVCFactory to create models * CS --- .../com_actionlogs/src/Plugin/ActionLogPlugin.php | 7 ++++--- administrator/components/com_admin/script.php | 4 +++- .../src/Helper/LatestActionsHelper.php | 7 ++++--- administrator/modules/mod_messages/mod_messages.php | 6 ++++-- .../mod_post_installation_messages.php | 4 +++- components/com_contact/src/View/Contact/HtmlView.php | 4 +++- plugins/content/joomla/joomla.php | 5 +++-- plugins/sampledata/blog/blog.php | 10 ++++++---- plugins/system/fields/fields.php | 9 +++++++-- 9 files changed, 37 insertions(+), 19 deletions(-) diff --git a/administrator/components/com_actionlogs/src/Plugin/ActionLogPlugin.php b/administrator/components/com_actionlogs/src/Plugin/ActionLogPlugin.php index 31eb8b46126bb..77bc08df63afa 100644 --- a/administrator/components/com_actionlogs/src/Plugin/ActionLogPlugin.php +++ b/administrator/components/com_actionlogs/src/Plugin/ActionLogPlugin.php @@ -13,7 +13,6 @@ use Joomla\CMS\Factory; use Joomla\CMS\Plugin\CMSPlugin; -use Joomla\Component\Actionlogs\Administrator\Model\ActionlogModel; /** * Abstract Action Log Plugin @@ -94,8 +93,10 @@ protected function addLog($messages, $messageLanguageKey, $context, $userId = nu $messages[$index] = $message; } - /** @var ActionlogModel $model */ - $model = new ActionlogModel; + /** @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/script.php b/administrator/components/com_admin/script.php index e2a691deb1f6c..bb973eb5cf2dd 100644 --- a/administrator/components/com_admin/script.php +++ b/administrator/components/com_admin/script.php @@ -7349,7 +7349,9 @@ public function convertTablesToUtf8mb4($doDbFixMsg = false) */ private function cleanJoomlaCache() { - $model = new \Joomla\Component\Cache\Administrator\Model\CacheModel; + /** @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(); diff --git a/administrator/modules/mod_latestactions/src/Helper/LatestActionsHelper.php b/administrator/modules/mod_latestactions/src/Helper/LatestActionsHelper.php index 0b9841d98e2ea..61aae97beef10 100644 --- a/administrator/modules/mod_latestactions/src/Helper/LatestActionsHelper.php +++ b/administrator/modules/mod_latestactions/src/Helper/LatestActionsHelper.php @@ -11,9 +11,9 @@ \defined('_JEXEC') or die; +use Joomla\CMS\Factory; use Joomla\CMS\Language\Text; use Joomla\Component\Actionlogs\Administrator\Helper\ActionlogsHelper; -use Joomla\Component\Actionlogs\Administrator\Model\ActionlogsModel; use Joomla\Registry\Registry; /** @@ -36,8 +36,9 @@ abstract class LatestActionsHelper */ public static function getList(&$params) { - /** @var ActionlogsModel $model */ - $model = new ActionlogsModel(['ignore_request' => true]); + /** @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); diff --git a/administrator/modules/mod_messages/mod_messages.php b/administrator/modules/mod_messages/mod_messages.php index 6fff2d4746a00..296f4e1109a59 100644 --- a/administrator/modules/mod_messages/mod_messages.php +++ b/administrator/modules/mod_messages/mod_messages.php @@ -20,9 +20,11 @@ // Try to get the items from the messages model try { - $messagesModel = new \Joomla\Component\Messages\Administrator\Model\MessagesModel(['ignore_request' => true]); + /** @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(); + $messages = $messagesModel->getItems(); } catch (RuntimeException $e) { 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 84ad89edc6dd1..981efac75c937 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 @@ -15,7 +15,9 @@ // Try to get the items from the post-installation model try { - $messagesModel = new \Joomla\Component\Postinstall\Administrator\Model\MessagesModel(['ignore_request' => true]); + /** @var \Joomla\Component\Postinstall\Administrator\Model\MessagesModel $messagesModel */ + $messagesModel = $app->bootComponent('com_postinstall')->getMVCFactory() + ->createModel('Messages', 'Administrator', ['ignore_request' => true]); $messages = $messagesModel->getItems(); } catch (RuntimeException $e) diff --git a/components/com_contact/src/View/Contact/HtmlView.php b/components/com_contact/src/View/Contact/HtmlView.php index 957c3adbbe200..a815fd9e3398f 100644 --- a/components/com_contact/src/View/Contact/HtmlView.php +++ b/components/com_contact/src/View/Contact/HtmlView.php @@ -159,7 +159,9 @@ public function display($tpl = null) if ($item && $item->params->get('show_contact_list')) { // Get Category Model data - $categoryModel = new \Joomla\Component\Contact\Site\Model\CategoryModel(array('ignore_request' => true)); + /** @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'); diff --git a/plugins/content/joomla/joomla.php b/plugins/content/joomla/joomla.php index 4c493c329ee93..0b764efe3bf2b 100644 --- a/plugins/content/joomla/joomla.php +++ b/plugins/content/joomla/joomla.php @@ -17,7 +17,6 @@ use Joomla\CMS\Table\CoreContent; use Joomla\CMS\User\User; use Joomla\CMS\Workflow\WorkflowServiceInterface; -use Joomla\Component\Workflow\Administrator\Model\StagesModel; use Joomla\Component\Workflow\Administrator\Table\StageTable; use Joomla\Component\Workflow\Administrator\Table\WorkflowTable; use Joomla\Database\DatabaseDriver; @@ -351,7 +350,9 @@ private function _canDeleteWorkflow($pk) return true; } - $model = new StagesModel(['ignore_request' => true]); + /** @var \Joomla\Component\Workflow\Administrator\Model\StagesModel $model */ + $model = $this->app->bootComponent('com_workflow')->getMVCFactory() + ->createModel('Stages', 'Administrator', ['ignore_request' => true]); $model->setState('filter.workflow_id', $pk); $model->setState('filter.extension', $table->extension); diff --git a/plugins/sampledata/blog/blog.php b/plugins/sampledata/blog/blog.php index 347291dbd851f..b1c8f671db7e6 100644 --- a/plugins/sampledata/blog/blog.php +++ b/plugins/sampledata/blog/blog.php @@ -19,7 +19,6 @@ use Joomla\CMS\Plugin\CMSPlugin; use Joomla\CMS\Plugin\PluginHelper; use Joomla\CMS\Session\Session; -use Joomla\Component\Menus\Administrator\Model\ItemModel; use Joomla\Database\ParameterType; /** @@ -59,7 +58,7 @@ class PlgSampledataBlog extends CMSPlugin /** * Holds the menuitem model * - * @var ItemModel + * @var \Joomla\Component\Menus\Administrator\Model\ItemModel * * @since 3.8.0 */ @@ -884,7 +883,8 @@ public function onAjaxSampledataApplyStep2() $articleIds = $this->app->getUserState('sampledata.blog.articles'); // Get MenuItemModel. - $this->menuItemModel = new ItemModel; + $this->menuItemModel = $this->app->bootComponent('com_menus')->getMVCFactory() + ->createModel('Item', 'Administrator', ['ignore_request' => true]); // Get previously entered categories ids $catIds = $this->app->getUserState('sampledata.blog.articles.catIds'); @@ -1410,7 +1410,9 @@ public function onAjaxSampledataApplyStep3() $langSuffix = ($language !== '*') ? ' (' . $language . ')' : ''; // Add Include Paths. - $model = new \Joomla\Component\Modules\Administrator\Model\ModuleModel; + /** @var \Joomla\Component\Modules\Administrator\Model\ModuleModel $model */ + $model = $this->app->bootComponent('com_modules')->getMVCFactory() + ->createModel('Module', 'Administrator', ['ignore_request' => true]); $access = (int) $this->app->get('access', 1); // Get previously entered Data from UserStates. diff --git a/plugins/system/fields/fields.php b/plugins/system/fields/fields.php index ace16c0695f5a..0b16170067820 100644 --- a/plugins/system/fields/fields.php +++ b/plugins/system/fields/fields.php @@ -130,7 +130,10 @@ public function onContentAfterSave($context, $item, $isNew, $data = array()): vo } // Loading the model - $model = new \Joomla\Component\Fields\Administrator\Model\FieldModel(array('ignore_request' => true)); + + /** @var \Joomla\Component\Fields\Administrator\Model\FieldModel $model */ + $model = Factory::getApplication()->bootComponent('com_fields')->getMVCFactory() + ->createModel('Field', 'Administrator', ['ignore_request' => true]); // Loop over the fields foreach ($fields as $field) @@ -219,7 +222,9 @@ public function onContentAfterDelete($context, $item): void $context = $parts[0] . '.' . $parts[1]; - $model = new \Joomla\Component\Fields\Administrator\Model\FieldModel(array('ignore_request' => true)); + /** @var \Joomla\Component\Fields\Administrator\Model\FieldModel $model */ + $model = Factory::getApplication()->bootComponent('com_fields')->getMVCFactory() + ->createModel('Field', 'Administrator', ['ignore_request' => true]); $model->cleanupValues($context, $item->id); } From c8e2cd52a40bb33dd62122eed7336eb0069b1ea4 Mon Sep 17 00:00:00 2001 From: Fedir Zinchuk Date: Sun, 30 May 2021 08:56:38 +0300 Subject: [PATCH 09/57] Fix row selecting, when module is disabled (#34273) --- administrator/components/com_modules/tmpl/modules/default.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/administrator/components/com_modules/tmpl/modules/default.php b/administrator/components/com_modules/tmpl/modules/default.php index 05a53b5b4da75..bd95ae2d29c9f 100644 --- a/administrator/components/com_modules/tmpl/modules/default.php +++ b/administrator/components/com_modules/tmpl/modules/default.php @@ -94,9 +94,7 @@ ?> - enabled > 0) : ?> - id, false, 'cid', 'cb', $item->title); ?> - + id, false, 'cid', 'cb', $item->title); ?> Date: Fri, 28 May 2021 15:57:30 +0200 Subject: [PATCH 10/57] Media web service implementation. --- .../en-GB/plg_webservices_content.sys.ini | 7 - .../language/en-GB/plg_webservices_media.ini | 7 + .../en-GB/plg_webservices_media.sys.ini | 7 + .../src/Controller/MediaController.php | 393 ++++++++++++++++++ .../com_media/src/Helper/AdapterTrait.php | 61 +++ .../com_media/src/Helper/MediaHelper.php | 51 +++ .../com_media/src/Model/MediaModel.php | 113 +++++ .../com_media/src/Model/MediumModel.php | 109 +++++ .../com_media/src/View/Media/JsonapiView.php | 105 +++++ plugins/webservices/media/media.php | 64 +++ plugins/webservices/media/media.xml | 19 + 11 files changed, 929 insertions(+), 7 deletions(-) delete mode 100644 administrator/language/en-GB/plg_webservices_content.sys.ini create mode 100644 administrator/language/en-GB/plg_webservices_media.ini create mode 100644 administrator/language/en-GB/plg_webservices_media.sys.ini create mode 100644 api/components/com_media/src/Controller/MediaController.php create mode 100644 api/components/com_media/src/Helper/AdapterTrait.php create mode 100644 api/components/com_media/src/Helper/MediaHelper.php create mode 100644 api/components/com_media/src/Model/MediaModel.php create mode 100644 api/components/com_media/src/Model/MediumModel.php create mode 100644 api/components/com_media/src/View/Media/JsonapiView.php create mode 100644 plugins/webservices/media/media.php create mode 100644 plugins/webservices/media/media.xml diff --git a/administrator/language/en-GB/plg_webservices_content.sys.ini b/administrator/language/en-GB/plg_webservices_content.sys.ini deleted file mode 100644 index 6202874af339f..0000000000000 --- a/administrator/language/en-GB/plg_webservices_content.sys.ini +++ /dev/null @@ -1,7 +0,0 @@ -; Joomla! Project -; (C) 2019 Open Source Matters, Inc. -; License GNU General Public License version 2 or later; see LICENSE.txt -; Note : All ini files need to be saved as UTF-8 - -PLG_WEBSERVICES_CONTENT="Web Services - Content" -PLG_WEBSERVICES_CONTENT_XML_DESCRIPTION="Add article routes to the API for your website." diff --git a/administrator/language/en-GB/plg_webservices_media.ini b/administrator/language/en-GB/plg_webservices_media.ini new file mode 100644 index 0000000000000..ec0a32f2a204f --- /dev/null +++ b/administrator/language/en-GB/plg_webservices_media.ini @@ -0,0 +1,7 @@ +; Joomla! Project +; (C) 20211 Open Source Matters, Inc. +; License GNU General Public License version 2 or later; see LICENSE.txt +; Note : All ini files need to be saved as UTF-8 + +PLG_WEBSERVICES_MEDIA="Web Services - Media" +PLG_WEBSERVICES_MEDIA_XML_DESCRIPTION="Add media routes to the API for your website." diff --git a/administrator/language/en-GB/plg_webservices_media.sys.ini b/administrator/language/en-GB/plg_webservices_media.sys.ini new file mode 100644 index 0000000000000..b2b25bba1114b --- /dev/null +++ b/administrator/language/en-GB/plg_webservices_media.sys.ini @@ -0,0 +1,7 @@ +; Joomla! Project +; (C) 2021 Open Source Matters, Inc. +; License GNU General Public License version 2 or later; see LICENSE.txt +; Note : All ini files need to be saved as UTF-8 + +PLG_WEBSERVICES_MEDIA="Web Services - Media" +PLG_WEBSERVICES_MEDIA_XML_DESCRIPTION="Add media routes to the API for your website." diff --git a/api/components/com_media/src/Controller/MediaController.php b/api/components/com_media/src/Controller/MediaController.php new file mode 100644 index 0000000000000..5c24fc74ffd85 --- /dev/null +++ b/api/components/com_media/src/Controller/MediaController.php @@ -0,0 +1,393 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\Component\Media\Api\Controller; + +\defined('_JEXEC') or die; + +use Joomla\CMS\Access\Exception\NotAllowed; +use Joomla\CMS\Application\Exception\NotAcceptable; +use Joomla\CMS\Component\ComponentHelper; +use Joomla\CMS\Filter\InputFilter; +use Joomla\CMS\Language\Text; +use Joomla\CMS\MVC\Controller\ApiController; +use Joomla\CMS\MVC\Controller\Exception\ResourceNotFound; +use Joomla\Component\Media\Administrator\Exception\FileExistsException; +use Joomla\Component\Media\Administrator\Exception\InvalidPathException; +use Joomla\Component\Media\Api\Helper\AdapterTrait; +use Joomla\Component\Media\Api\Helper\MediaHelper; +use Joomla\String\Inflector; + +/** + * Media web service controller. + * + * @since 4.0.0 + */ +class MediaController extends ApiController +{ + use AdapterTrait; + + /** + * The content type of the item. + * + * @var string + * @since 4.0.0 + */ + protected $contentType = 'media'; + + /** + * Query parameters => model state mappings + * + * @var array + */ + private static $listQueryModelStateMap = [ + 'path' => [ + 'name' => 'path', + 'type' => 'PATH' + ], + 'url' => [ + 'name' => 'url', + 'type' => 'BOOLEAN' + ], + 'temp' => [ + 'name' => 'temp', + 'type' => 'BOOLEAN' + ], + 'content' => [ + 'name' => 'content', + 'type' => 'BOOLEAN' + ], + ]; + + private static $itemQueryModelStateMap = [ + 'path' => [ + 'name' => 'path', + 'type' => 'PATH' + ], + 'url' => [ + 'name' => 'url', + 'type' => 'BOOLEAN' + ], + 'temp' => [ + 'name' => 'temp', + 'type' => 'BOOLEAN' + ], + 'content' => [ + 'name' => 'content', + 'type' => 'BOOLEAN' + ], + ]; + + /** + * The default view for the display method. + * + * @var string + * + * @since 3.0 + */ + protected $default_view = 'media'; + + /** + * Execute a task by triggering a method in the derived class. + * This method overrides the base method, to enable mapping of com_media exceptions to API handled exceptions. + * + * @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.0 + */ + public function execute($task) + { + // Execute parent method and catch com_media specific exceptions and map them to API equivalents. + try + { + parent::execute($task); + } + // A specific file or folder was requested or meant to be updated or deleted. + catch (InvalidPathException $e) + { + throw new ResourceNotFound(); + } + // A file or folder was meant to be created, but it already exists and overwriting is not the intention. + catch (FileExistsException $e) + { + throw new NotAcceptable(); + } + catch (\Exception $e) + { + throw $e; + } + } + + /** + * Display a list of files and/or folders. + * + * @return static A \JControllerLegacy object to support chaining. + * + * @throws \Exception + * + * @since 4.0.0 + */ + public function displayList() + { + // Set list specific request parameters in model state. + $this->setModelState(self::$listQueryModelStateMap); + + // Map JSON:API compliant filter[search] to com_media model state. + $apiFilterInfo = $this->input->get('filter', [], 'array'); + $filter = InputFilter::getInstance(); + + // Tell model to display files in specific path. + if (array_key_exists('path', $apiFilterInfo)) + { + $this->modelState->set('path', $filter->clean($apiFilterInfo['path'], 'PATH')); + } + + // Tell model to search for files matching (part of) a name or glob pattern. + if ($doSearch = array_key_exists('search', $apiFilterInfo)) + { + $this->modelState->set('search', $filter->clean($apiFilterInfo['search'], 'STRING')); + // Tell model to search recursively + $this->modelState->set('search_recursive', $this->input->get('search_recursive', false, 'BOOLEAN')); + } + + return parent::displayList(); + } + + /** + * Display one specific file or folder. + * + * @param string $path The path of the file to display. Leave empty if you want to retrieve data from the request. + * + * @return static A \JControllerLegacy object to support chaining. + * + * @throws \InvalidPathException + * @throws \Exception + * + * @since 4.0.0 + */ + public function displayItem($path = null) + { + // Set list specific request parameters in model state. + $this->setModelState(self::$itemQueryModelStateMap); + + // Tell model which file to dsplay. + if ($path) + { + $this->modelState->set('path', $path); + } + + return parent::displayItem(); + } + + /** + * Set model state using a list of mappings between query parameters and model state names. + * + * @param array $mappings A list of mappings between query parameters and model state names.. + * + * @return void + * + * @since 4.0.0 + */ + private function setModelState(array $mappings) + { + foreach ($mappings as $queryName => $modelState) + { + if ($this->input->exists($queryName)) + { + $this->modelState->set($modelState['name'], $this->input->get($queryName, '', $modelState['type'])); + } + } + } + + /** + * Method to add a new file or folder. + * + * @return void + * + * @throws FileExistsException + * @throws InvalidPathException + * @throws \RuntimeException + * @throws \Exception + * + * @since 4.0.0 + * @since 4.0.0 + */ + public function add() + { + // Check if an existing file may be overwritten. Defaults to false. + $this->input->set('path', $this->input->json->get('path')); + $this->modelState->set('override', $this->input->json->get('override', false)); + + parent::add(); + } + + /** + * Method to check if it's allowed to add a new file or folder + * + * @param array $data An array of input data. + * + * @return boolean + * + * @since 4.0.0 + */ + protected function allowAdd($data = array()) + { + $user = $this->app->getIdentity(); + + return $user->authorise('core.create', 'com_media'); + } + + /** + * Method to modify an existing file or folder. + * + * @return void + * + * @throws FileExistsException + * @throws InvalidPathException + * @throws \RuntimeException + * @throws \Exception + * + * @since 4.0.0 + */ + public function edit() + { + // Access check. + if (!$this->allowEdit()) + { + throw new NotAllowed('JLIB_APPLICATION_ERROR_CREATE_RECORD_NOT_PERMITTED', 403); + } + + // Check if an existing file may be overwritten. Defaults to true. + $this->modelState->set('override', $this->input->json->get('override', true)); + $recordId = $this->save(); + + $this->displayItem($recordId); + } + + /** + * Method to check if it's allowed to modify an existing file or folder. + * + * @param array $data An array of input data. + * + * @return boolean + * + * @since 4.0.0 + */ + protected function allowEdit($data = array(), $key = 'id') + { + $user = $this->app->getIdentity(); + + // com_media's access rules contains no specific update rule. + return $user->authorise('core.create', 'com_media'); + } + + /** + * Method to create or modify a file or folder. + * + * @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) + { + // Explicitly get the single item model name. + $modelName = $this->input->get('model', Inflector::singularize($this->contentType)); + $model = $this->getModel($modelName, '', ['ignore_request' => true, 'state' => $this->modelState]); + + $json = $this->input->json; + + // Split destination path into adapter name and file path. + list('adapter' => $adapter, 'path' => $path) = MediaHelper::adapterNameAndPath($this->input->get('path', '', 'PATH')); + + // Decode content, if any + if ($content = base64_decode($json->get('content', '', 'raw'))) + { + $this->checkContent(); + } + + // If there is no content, com_media's assumes the path refers to a folder. + $this->modelState->set('content', $content); + // com_media expects separate directory and file name. + $this->modelState->set('name', basename($path)); + $this->modelState->set('path', dirname($path)); + + return $model->save(); + } + + /** + * Performs various checks to see if it is allowed to save the content. + * + * @return void + * + * @throws \RuntimeException + * + * @since 4.0.0 + */ + private function checkContent() + { + $params = ComponentHelper::getParams('com_media'); + $helper = new \Joomla\CMS\Helper\MediaHelper(); + $serverlength = $this->input->server->getInt('CONTENT_LENGTH'); + + // Check if the size of the request body does not exceed various server imposed limits. + if (($params->get('upload_maxsize', 0) > 0 && $serverlength > ($params->get('upload_maxsize', 0) * 1024 * 1024)) + || $serverlength > $helper->toBytes(ini_get('upload_max_filesize')) + || $serverlength > $helper->toBytes(ini_get('post_max_size')) + || $serverlength > $helper->toBytes(ini_get('memory_limit'))) + { + throw new \RuntimeException(Text::_('COM_MEDIA_ERROR_WARNFILETOOLARGE'), 400); + } + } + + /** + * Method to delete an existing file or folder. + * + * @return void + * + * @throws InvalidPathException + * @throws \RuntimeException + * @throws \Exception + * + * @since 4.0.0 + */ + public function delete($id = null) + { + if (!$this->allowDelete()) + { + throw new NotAllowed('JLIB_APPLICATION_ERROR_DELETE_NOT_PERMITTED', 403); + } + + $this->modelState->set('path', $this->input->get('path', '', 'PATH')); + + $modelName = $this->input->get('model', Inflector::singularize($this->contentType)); + $model = $this->getModel($modelName, '', ['ignore_request' => true, 'state' => $this->modelState]); + + $model->delete(); + } + + /** + * Method to check if it's allowed to delete an existing file or folder. + * + * @return boolean + * + * @since 4.0.0 + */ + protected function allowDelete() + { + $user = $this->app->getIdentity(); + + return $user->authorise('core.delete', 'com_media'); + } +} diff --git a/api/components/com_media/src/Helper/AdapterTrait.php b/api/components/com_media/src/Helper/AdapterTrait.php new file mode 100644 index 0000000000000..899a674b7e833 --- /dev/null +++ b/api/components/com_media/src/Helper/AdapterTrait.php @@ -0,0 +1,61 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\Component\Media\Api\Helper; + +\defined('_JEXEC') or die; + +use Joomla\CMS\Factory; +use Joomla\CMS\Plugin\PluginHelper; +use Joomla\Component\Media\Administrator\Adapter\AdapterInterface; +use Joomla\Component\Media\Administrator\Event\MediaProviderEvent; +use Joomla\Component\Media\Administrator\Provider\ProviderManager; + +/** + * Trait for classes that need adapters. + * + * @since 4.0.0 + */ +trait AdapterTrait +{ + /** + * Holds the available media file adapters. + * + * @var ProviderManager + * + * @since 4.0.0 + */ + private $providerManager = null; + + /** + * Return a provider manager. + * + * @return AdapterInterface + * + * @throws \Exception + * + * @since 4.0.0 + */ + private function getAdapter(String $name) + { + if (!$this->providerManager) + { + $this->providerManager = new ProviderManager(); + + // Fire the event to get the results + $eventParameters = ['context' => 'AdapterManager', 'providerManager' => $this->providerManager]; + $event = new MediaProviderEvent('onSetupProviders', $eventParameters); + PluginHelper::importPlugin('filesystem'); + Factory::getApplication()->triggerEvent('onSetupProviders', $event); + } + + return $this->providerManager->getAdapter($name); + } + +} diff --git a/api/components/com_media/src/Helper/MediaHelper.php b/api/components/com_media/src/Helper/MediaHelper.php new file mode 100644 index 0000000000000..b4a6e4f35e56e --- /dev/null +++ b/api/components/com_media/src/Helper/MediaHelper.php @@ -0,0 +1,51 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\Component\Media\Api\Helper; + +\defined('_JEXEC') or die; + +/** + * Helper methods for media web service. + * + * @since 4.0.0 + */ +class MediaHelper +{ + /** + * Split a given path in adapter prefix and file path. + * + * @param string $path The path to split. + * + * @return array An array with elements 'adapter' and 'path'. + * + * @since 4.0 + */ + public static function adapterNameAndPath(String $path) + { + $result = []; + $parts = explode(':', $path, 2); + + // If we have 2 parts, we have both an adapetr name and a file path. + if (count($parts) == 2) + { + $result['adapter'] = $parts[0]; + $result['path'] = $parts[1]; + + return $result; + } + + // If we have less than 2 parts, we return a default aadapter name. + $result['adapter'] = 'local-images'; + // 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; + } +} diff --git a/api/components/com_media/src/Model/MediaModel.php b/api/components/com_media/src/Model/MediaModel.php new file mode 100644 index 0000000000000..34164caa1d537 --- /dev/null +++ b/api/components/com_media/src/Model/MediaModel.php @@ -0,0 +1,113 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\Component\Media\Api\Model; + +\defined('_JEXEC') or die; + +use Joomla\CMS\MVC\Model\BaseModel; +use Joomla\CMS\MVC\Model\ListModelInterface; +use Joomla\CMS\Pagination\Pagination; +use Joomla\Component\Media\Administrator\Model\ApiModel; +use Joomla\Component\Media\Api\Helper\MediaHelper; + +/** + * Media web service model supporting lists of media items. + * + * @since 4.0 + */ +class MediaModel extends BaseModel implements ListModelInterface +{ + /** + * Instance of com_media's ApiModel + * + * @var ApiModel + */ + private $mediaApiModel; + + /* + * A hacky way to enable the standard jsonapiView::displayList() to create a Pagination object, + * since com_media's ApiModel does not support pagination as we know from regular ListModel derived models. + */ + private $total = 0; + + public function __construct($config = []) + { + parent::__construct($config); + + $this->mediaApiModel = new ApiModel(); + } + + /** + * Method to get a list of files and/or folders. + * + * @return array An array of data items. + * + * @since 4.0.0 + */ + public function getItems() + { + // Map web service model state to com_media options. + $options = [ + 'url' => $this->getState('url', false), + 'temp' => $this->getState('temp', false), + 'search' => $this->getState('search', ''), + 'recursive' => $this->getState('search_recursive', false), + 'content' => $this->getState('content', false) + ]; + + list('adapter' => $adapterName, 'path' => $path) = MediaHelper::adapterNameAndPath($this->getState('path', '')); + $files = $this->mediaApiModel->getFiles($adapterName, $path, $options); + + // A hacky way to enable the standard jsonapiView::displayList() to create a Pagination object. + // Because com_media's ApiModel does not support pagination as we know from regular ListModel + // derived models, we always return all retrieved items. + $this->total = count($files); + + return $files; + } + + /** + * Method to get a \JPagination object for the data set. + * + * @return Pagination A Pagination object for the data set. + * + * @since 4.0 + */ + public function getPagination() + { + return new Pagination($this->getTotal(), $this->getStart(), 0);; + } + + /** + * Method to get the starting number of items for the data set. Because com_media's ApiModel + * does not support pagination as we know from regular ListModel derived models, + * we always start at the top. + * + * @return integer The starting number of items available in the data set. + * + * @since 4.0 + */ + public function getStart() + { + return 0; + } + + /** + * Method to get the total number of items for the data set. + * + * @return integer The total number of items available in the data set. + * + * @since 1.6 + */ + public function getTotal() + { + return $this->total; + } +} diff --git a/api/components/com_media/src/Model/MediumModel.php b/api/components/com_media/src/Model/MediumModel.php new file mode 100644 index 0000000000000..2c52e2e65fdfb --- /dev/null +++ b/api/components/com_media/src/Model/MediumModel.php @@ -0,0 +1,109 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\Component\Media\Api\Model; + +\defined('_JEXEC') or die; + +use Joomla\CMS\MVC\Model\BaseModel; +use Joomla\Component\Media\Administrator\Model\ApiModel; +use Joomla\Component\Media\Api\Helper\MediaHelper; + +/** + * Media web service model supporting a single media item. + * + * @since 4.0 + */ +class MediumModel extends BaseModel +{ + /** + * Instance of com_media's ApiModel + * + * @var ApiModel + */ + private $mediaApiModel; + + public function __construct($config = []) + { + parent::__construct($config); + + $this->mediaApiModel = new ApiModel(); + } + + /** + * Method to get a single files or folder. + * + * @return \stdClass A file or folder object. + * + * @since 4.0.0 + */ + public function getItem() + { + $options = [ + 'path' => $this->getState('path', ''), + 'url' => $this->getState('url', false), + 'temp' => $this->getState('temp', false), + 'content' => $this->getState('content', false) + ]; + + list('adapter' => $adapterName, 'path' => $path) = MediaHelper::adapterNameAndPath($this->getState('path', '')); + + return $this->mediaApiModel->getFile($adapterName, $path, $options = []); + } + + /** + * Method to save a file or folder. + * + * @param string $path The primary key of the item (if exists) + * + * @return integer The record ID on success, false on failure + * + * @since 4.0.0 + */ + public function save($path = null) + { + $name = $this->getState('name', ''); + $path = $this->getState('path', ''); + $content = $this->getState('content', null); + $override = $this->getState('override', false); + + list('adapter' => $adapter, 'path' => $path) = MediaHelper::adapterNameAndPath($path); + + // If there is content, com_media's assumes the path refers to a file. + // If not, a folder is assumed. + if ($content) + { + // A file needs to be created + $name = $this->mediaApiModel->createFile($adapter, $name, $path, $content, $override); + } + else + { + // A file needs to be created + $name = $this->mediaApiModel->createFolder($adapter, $name, $path, $override); + } + + return $path . '/' . $name; + } + + /** + * Method to delete an existing file or folder. + * + * @return void + * + * @since 4.0.0 + */ + public function delete() + { + $path = $this->getState('path', ''); + + list('adapter' => $adapterName, 'path' => $path) = MediaHelper::adapterNameAndPath($path); + + $this->mediaApiModel->delete($adapterName, $path); + } +} diff --git a/api/components/com_media/src/View/Media/JsonapiView.php b/api/components/com_media/src/View/Media/JsonapiView.php new file mode 100644 index 0000000000000..dcf1653d10c84 --- /dev/null +++ b/api/components/com_media/src/View/Media/JsonapiView.php @@ -0,0 +1,105 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\Component\Media\Api\View\Media; + +\defined('_JEXEC') or die; + +use Joomla\CMS\MVC\View\JsonApiView as BaseApiView; +use Joomla\Component\Media\Administrator\Provider\ProviderManager; +use Joomla\Component\Media\Api\Helper\AdapterTrait; +use Joomla\Component\Media\Api\Helper\MediaHelper; + +/** + * Media web service view + * + * @since 4.0.0 + */ +class JsonapiView extends BaseApiView +{ + use AdapterTrait; + + /** + * The fields to render item in the documents + * + * @var array + * @since 4.0.0 + */ + protected $fieldsToRenderItem = [ + 'type', + 'name', + 'path', + 'extension', + 'size', + 'mime_type', + 'width', + 'height', + 'create_date', + 'create_date_formatted', + 'modified_date', + 'modified_date_formatted', + 'thumb_path', + 'adapter', + 'content' + ]; + + /** + * The fields to render items in the documents + * + * @var array + * @since 4.0.0 + */ + protected $fieldsToRenderList = [ + 'type', + 'name', + 'path', + 'extension', + 'size', + 'mime_type', + 'width', + 'height', + 'create_date', + 'create_date_formatted', + 'modified_date', + 'modified_date_formatted', + 'thumb_path', + 'adapter', + 'content' + ]; + + /** + * Holds the available media file adapters. + * + * @var ProviderManager + * @since 4.0.0 + */ + private $providerManager = null; + + /** + * Prepare item before render. + * + * @param object $item The model item + * + * @return object + * + * @since 4.0.0 + */ + protected function prepareItem($item) + { + // Media resources have no id. + $item->id = '0'; + + // Transform resource location into url. + list('adapter' => $adapterName, 'path' => $path) = MediaHelper::adapterNameAndPath($item->path); + $adapter = $this->getAdapter($adapterName); + $item->path = $adapter->getUrl($path); + + return $item; + } +} diff --git a/plugins/webservices/media/media.php b/plugins/webservices/media/media.php new file mode 100644 index 0000000000000..14be21b996255 --- /dev/null +++ b/plugins/webservices/media/media.php @@ -0,0 +1,64 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +defined('_JEXEC') or die; + +use Joomla\CMS\Plugin\CMSPlugin; +use Joomla\CMS\Router\ApiRouter; +use Joomla\Router\Route; + +/** + * Web Services adapter for com_media. + * + * @since 4.0.0 + */ +class PlgWebservicesMedia extends CMSPlugin +{ + /** + * Load the language file on instantiation. + * + * @var boolean + * @since 4.0.0 + */ + protected $autoloadLanguage = true; + + /** + * Registers com_media's API's routes in the application + * + * @param ApiRouter &$router The API Routing object + * + * @return void + * + * @since 4.0.0 + */ + public function onBeforeApiRoute(&$router) + { + $this->createCRUDRoutes( + $router, + 'v1/media', + 'media', + ['component' => 'com_media'] + ); + } + + private function createCRUDRoutes(&$router, $baseName, $controller, $defaults = [], $publicGets = false) + { + $getDefaults = array_merge(['public' => $publicGets], $defaults); + + $routes = [ + new Route(['GET'], $baseName, $controller . '.displayList', [], $getDefaults), + new Route(['GET'], $baseName . '/:path', $controller . '.displayItem', ['path' => '.*'], $getDefaults), + new Route(['POST'], $baseName, $controller . '.add', [], $defaults), + new Route(['PATCH'], $baseName . '/:path', $controller . '.edit', ['path' => '.*'], $defaults), + new Route(['DELETE'], $baseName . '/:path', $controller . '.delete', ['path' => '.*'], $defaults), + ]; + + $router->addRoutes($routes); + } +} diff --git a/plugins/webservices/media/media.xml b/plugins/webservices/media/media.xml new file mode 100644 index 0000000000000..6184eab57f736 --- /dev/null +++ b/plugins/webservices/media/media.xml @@ -0,0 +1,19 @@ + + + plg_webservices_media + Joomla! Project + May 2021 + (C) 2021 Open Source Matters, Inc. + GNU General Public License version 2 or later; see LICENSE.txt + admin@joomla.org + www.joomla.org + 4.0.0 + PLG_WEBSERVICES_MEDIA_XML_DESCRIPTION + + media.php + + + language/en-GB/en-GB.plg_webservices_media.ini + language/en-GB/en-GB.plg_webservices_media.sys.ini + + From 8466992eac402e55f2c339af9745a2e28a5fe9f7 Mon Sep 17 00:00:00 2001 From: Pieter-Jan de Vries Date: Mon, 31 May 2021 14:43:42 +0200 Subject: [PATCH 11/57] Media web service implementation. --- .../src/Controller/MediaController.php | 56 ++++++---- .../com_media/src/Helper/AdapterTrait.php | 1 - .../com_media/src/Model/MediumModel.php | 105 +++++++++++++----- .../com_media/src/View/Media/JsonapiView.php | 12 +- 4 files changed, 119 insertions(+), 55 deletions(-) diff --git a/api/components/com_media/src/Controller/MediaController.php b/api/components/com_media/src/Controller/MediaController.php index 5c24fc74ffd85..cd5fab18ed428 100644 --- a/api/components/com_media/src/Controller/MediaController.php +++ b/api/components/com_media/src/Controller/MediaController.php @@ -19,6 +19,7 @@ use Joomla\CMS\MVC\Controller\ApiController; use Joomla\CMS\MVC\Controller\Exception\ResourceNotFound; use Joomla\Component\Media\Administrator\Exception\FileExistsException; +use Joomla\Component\Media\Administrator\Exception\FileNotFoundException; use Joomla\Component\Media\Administrator\Exception\InvalidPathException; use Joomla\Component\Media\Api\Helper\AdapterTrait; use Joomla\Component\Media\Api\Helper\MediaHelper; @@ -117,11 +118,16 @@ public function execute($task) { throw new ResourceNotFound(); } - // A file or folder was meant to be created, but it already exists and overwriting is not the intention. + // A file or folder was meant to be created, but it already exists and overwriting is not the intention. catch (FileExistsException $e) { throw new NotAcceptable(); } + // This exception is thrown when a filename + catch (FileNotFoundException $e) + { + throw new NotAcceptable(); + } catch (\Exception $e) { throw $e; @@ -142,17 +148,23 @@ public function displayList() // Set list specific request parameters in model state. $this->setModelState(self::$listQueryModelStateMap); - // Map JSON:API compliant filter[search] to com_media model state. - $apiFilterInfo = $this->input->get('filter', [], 'array'); - $filter = InputFilter::getInstance(); + // Display files in specific path. + if ($this->input->exists('path')) + { + $this->modelState->set('path', $this->input->get('path', '', 'PATH')); + } - // Tell model to display files in specific path. - if (array_key_exists('path', $apiFilterInfo)) + // Return files (not folders) as url's. + if ($this->input->exists('url')) { - $this->modelState->set('path', $filter->clean($apiFilterInfo['path'], 'PATH')); + $this->modelState->set('url', $this->input->get('url', true, 'BOOLEAN')); } - // Tell model to search for files matching (part of) a name or glob pattern. + // Map JSON:API compliant filter[search] to com_media model state. + $apiFilterInfo = $this->input->get('filter', [], 'array'); + $filter = InputFilter::getInstance(); + + // Search for files matching (part of) a name or glob pattern. if ($doSearch = array_key_exists('search', $apiFilterInfo)) { $this->modelState->set('search', $filter->clean($apiFilterInfo['search'], 'STRING')); @@ -170,20 +182,23 @@ public function displayList() * * @return static A \JControllerLegacy object to support chaining. * - * @throws \InvalidPathException + * @throws InvalidPathException * @throws \Exception * * @since 4.0.0 */ - public function displayItem($path = null) + public function displayItem($path = '') { // Set list specific request parameters in model state. $this->setModelState(self::$itemQueryModelStateMap); - // Tell model which file to dsplay. - if ($path) + // Display files in specific path. + $this->modelState->set('path', $path ?: $this->input->get('path', '', 'PATH')); + + // Return files (not folders) as url's. + if ($this->input->exists('url')) { - $this->modelState->set('path', $path); + $this->modelState->set('url', $this->input->get('url', true, 'BOOLEAN')); } return parent::displayItem(); @@ -224,8 +239,8 @@ private function setModelState(array $mappings) */ public function add() { + $this->modelState->set('path', $this->input->json->get('path', '', 'PATH')); // Check if an existing file may be overwritten. Defaults to false. - $this->input->set('path', $this->input->json->get('path')); $this->modelState->set('override', $this->input->json->get('override', false)); parent::add(); @@ -267,8 +282,14 @@ public function edit() throw new NotAllowed('JLIB_APPLICATION_ERROR_CREATE_RECORD_NOT_PERMITTED', 403); } + $path = $this->input->json->get('path', '', 'STRING'); + $path = $this->input->json->get('path', '', 'PATH'); + $this->modelState->set('path', $this->input->json->get('path', '', 'PATH')); + // For renaming/moving files, we need the path to the existing file or folder. + $this->modelState->set('old_path', $this->input->get('path', '', 'PATH')); // Check if an existing file may be overwritten. Defaults to true. - $this->modelState->set('override', $this->input->json->get('override', true)); + $this->modelState->set('override', $this->input->json->get('override', false)); + $recordId = $this->save(); $this->displayItem($recordId); @@ -309,7 +330,7 @@ protected function save($recordKey = null) $json = $this->input->json; // Split destination path into adapter name and file path. - list('adapter' => $adapter, 'path' => $path) = MediaHelper::adapterNameAndPath($this->input->get('path', '', 'PATH')); + ['adapter' => $adapter, 'path' => $path] = MediaHelper::adapterNameAndPath($this->input->get('path', '', 'PATH')); // Decode content, if any if ($content = base64_decode($json->get('content', '', 'raw'))) @@ -319,9 +340,6 @@ protected function save($recordKey = null) // If there is no content, com_media's assumes the path refers to a folder. $this->modelState->set('content', $content); - // com_media expects separate directory and file name. - $this->modelState->set('name', basename($path)); - $this->modelState->set('path', dirname($path)); return $model->save(); } diff --git a/api/components/com_media/src/Helper/AdapterTrait.php b/api/components/com_media/src/Helper/AdapterTrait.php index 899a674b7e833..192e1ba582088 100644 --- a/api/components/com_media/src/Helper/AdapterTrait.php +++ b/api/components/com_media/src/Helper/AdapterTrait.php @@ -57,5 +57,4 @@ private function getAdapter(String $name) return $this->providerManager->getAdapter($name); } - } diff --git a/api/components/com_media/src/Model/MediumModel.php b/api/components/com_media/src/Model/MediumModel.php index 2c52e2e65fdfb..6209f23126888 100644 --- a/api/components/com_media/src/Model/MediumModel.php +++ b/api/components/com_media/src/Model/MediumModel.php @@ -4,7 +4,8 @@ * @subpackage com_media * * @copyright (C) 2021 Open Source Matters, Inc. - * @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\Component\Media\Api\Model; @@ -14,14 +15,15 @@ use Joomla\CMS\MVC\Model\BaseModel; use Joomla\Component\Media\Administrator\Model\ApiModel; use Joomla\Component\Media\Api\Helper\MediaHelper; +use Tobscure\JsonApi\Exception\InvalidParameterException; /** * Media web service model supporting a single media item. * * @since 4.0 */ -class MediumModel extends BaseModel -{ +class MediumModel extends BaseModel { + /** * Instance of com_media's ApiModel * @@ -29,8 +31,7 @@ class MediumModel extends BaseModel */ private $mediaApiModel; - public function __construct($config = []) - { + public function __construct($config = []) { parent::__construct($config); $this->mediaApiModel = new ApiModel(); @@ -43,16 +44,18 @@ public function __construct($config = []) * * @since 4.0.0 */ - public function getItem() - { + public function getItem() { $options = [ 'path' => $this->getState('path', ''), - 'url' => $this->getState('url', false), - 'temp' => $this->getState('temp', false), - 'content' => $this->getState('content', false) + 'url' => $this->getState('url', FALSE), + 'temp' => $this->getState('temp', FALSE), + 'content' => $this->getState('content', FALSE), ]; - list('adapter' => $adapterName, 'path' => $path) = MediaHelper::adapterNameAndPath($this->getState('path', '')); + [ + 'adapter' => $adapterName, + 'path' => $path, + ] = MediaHelper::adapterNameAndPath($this->getState('path', '')); return $this->mediaApiModel->getFile($adapterName, $path, $options = []); } @@ -66,29 +69,68 @@ public function getItem() * * @since 4.0.0 */ - public function save($path = null) - { - $name = $this->getState('name', ''); + public function save($path = NULL) { $path = $this->getState('path', ''); - $content = $this->getState('content', null); - $override = $this->getState('override', false); + $oldPath = $this->getState('old_path', ''); + $content = $this->getState('content', NULL); + $override = $this->getState('override', FALSE); + + [ + 'adapter' => $adapterName, + 'path' => $path, + ] = MediaHelper::adapterNameAndPath($path); - list('adapter' => $adapter, 'path' => $path) = MediaHelper::adapterNameAndPath($path); + $resultPath = ''; - // If there is content, com_media's assumes the path refers to a file. - // If not, a folder is assumed. - if ($content) + // If we have a (new) path and an old path, we want to move an existing + // file or folder. This must be done before updating the content of a file, + // if also requested (see below). + if ($path && $oldPath) { - // A file needs to be created - $name = $this->mediaApiModel->createFile($adapter, $name, $path, $content, $override); + // ApiModel::move() (or actually LocalAdapter::move()) returns a path + // with leading slash. + $resultPath = trim($this->mediaApiModel->move($adapterName, $oldPath, $path, $override), '/'); } - else + + // If we have a (new) path but no old path, we want to create a + // new file or folder. + if ($path && !$oldPath) { - // A file needs to be created - $name = $this->mediaApiModel->createFolder($adapter, $name, $path, $override); + // com_media expects separate directory and file name. + // If we moved the file before, we must use the new path. + $basename = basename($resultPath ?: $path); + $dirname = dirname($resultPath ?: $path); + + // If there is content, com_media's assumes the new item is a file. + // Otherwise a folder is assumed. + $name = $content + ? $this->mediaApiModel->createFile($adapterName, $basename, $dirname, $content, $override) + : $this->mediaApiModel->createFolder($adapterName, $basename, $dirname, $override); + + $resultPath = $dirname . '/' . $name; } - return $path . '/' . $name; + // If we have no (new) path but we do have an old path and we have content, + // we want to update the contents of an existing file. + if ($oldPath && $content) + { + // com_media expects separate directory and file name. + // If we moved the file before, we must use the new path. + $basename = basename($resultPath ?: $oldPath); + $dirname = dirname($resultPath ?: $oldPath); + + $this->mediaApiModel->updateFile($adapterName, $basename, $dirname, $content); + + $resultPath = $oldPath; + } + + // If we still have no result path, something fishy is going on. + if (!$resultPath) + { + throw new InvalidParameterException(); + } + + return $resultPath; } /** @@ -98,12 +140,15 @@ public function save($path = null) * * @since 4.0.0 */ - public function delete() - { - $path = $this->getState('path', ''); + public function delete() { + $path = $this->getState('path', ''); - list('adapter' => $adapterName, 'path' => $path) = MediaHelper::adapterNameAndPath($path); + [ + 'adapter' => $adapterName, + 'path' => $path, + ] = MediaHelper::adapterNameAndPath($path); $this->mediaApiModel->delete($adapterName, $path); } + } diff --git a/api/components/com_media/src/View/Media/JsonapiView.php b/api/components/com_media/src/View/Media/JsonapiView.php index dcf1653d10c84..a6618b80df548 100644 --- a/api/components/com_media/src/View/Media/JsonapiView.php +++ b/api/components/com_media/src/View/Media/JsonapiView.php @@ -70,7 +70,9 @@ class JsonapiView extends BaseApiView 'modified_date_formatted', 'thumb_path', 'adapter', - 'content' + 'content', + 'url', + 'tempUrl' ]; /** @@ -95,10 +97,10 @@ protected function prepareItem($item) // Media resources have no id. $item->id = '0'; - // Transform resource location into url. - list('adapter' => $adapterName, 'path' => $path) = MediaHelper::adapterNameAndPath($item->path); - $adapter = $this->getAdapter($adapterName); - $item->path = $adapter->getUrl($path); +// // Transform resource location into url. +// list('adapter' => $adapterName, 'path' => $path) = MediaHelper::adapterNameAndPath($item->path); +// $adapter = $this->getAdapter($adapterName); +// $item->path = $adapter->getUrl($path); return $item; } From 5612a4c5e6fa1aedceea2e81e3c5f4b145506e3b Mon Sep 17 00:00:00 2001 From: Pieter-Jan de Vries Date: Wed, 2 Jun 2021 11:39:17 +0200 Subject: [PATCH 12/57] Fix hard coded adapter name default. --- .../com_media/src/Helper/MediaHelper.php | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/api/components/com_media/src/Helper/MediaHelper.php b/api/components/com_media/src/Helper/MediaHelper.php index b4a6e4f35e56e..d06e02d6d0813 100644 --- a/api/components/com_media/src/Helper/MediaHelper.php +++ b/api/components/com_media/src/Helper/MediaHelper.php @@ -9,6 +9,8 @@ namespace Joomla\Component\Media\Api\Helper; +use Joomla\CMS\Component\ComponentHelper; + \defined('_JEXEC') or die; /** @@ -42,10 +44,22 @@ public static function adapterNameAndPath(String $path) } // If we have less than 2 parts, we return a default aadapter name. - $result['adapter'] = 'local-images'; + $result['adapter'] = self::defaultAdapterName(); // 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; } + + private static function defaultAdapterName() + { + static $comMediaParams; + + if (!$comMediaParams) + { + $comMediaParams = ComponentHelper::getParams('com_media'); + } + + return 'local-' . $comMediaParams->get('file_path', 'images'); + } } From 6ba3655a8012f74b3a34d5f71c26253680e19b93 Mon Sep 17 00:00:00 2001 From: Pieter-Jan de Vries Date: Wed, 2 Jun 2021 12:31:25 +0200 Subject: [PATCH 13/57] Fix missing url and tempUrl attributes in single item response. --- api/components/com_media/src/Model/MediumModel.php | 2 +- api/components/com_media/src/View/Media/JsonapiView.php | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/api/components/com_media/src/Model/MediumModel.php b/api/components/com_media/src/Model/MediumModel.php index 6209f23126888..44860934dfb0b 100644 --- a/api/components/com_media/src/Model/MediumModel.php +++ b/api/components/com_media/src/Model/MediumModel.php @@ -57,7 +57,7 @@ public function getItem() { 'path' => $path, ] = MediaHelper::adapterNameAndPath($this->getState('path', '')); - return $this->mediaApiModel->getFile($adapterName, $path, $options = []); + return $this->mediaApiModel->getFile($adapterName, $path, $options); } /** diff --git a/api/components/com_media/src/View/Media/JsonapiView.php b/api/components/com_media/src/View/Media/JsonapiView.php index a6618b80df548..c2aab2fc57753 100644 --- a/api/components/com_media/src/View/Media/JsonapiView.php +++ b/api/components/com_media/src/View/Media/JsonapiView.php @@ -46,7 +46,9 @@ class JsonapiView extends BaseApiView 'modified_date_formatted', 'thumb_path', 'adapter', - 'content' + 'content', + 'url', + 'tempUrl' ]; /** From 77ad81095bd2e7217c7eb637edfc31404457693a Mon Sep 17 00:00:00 2001 From: Pieter-Jan de Vries Date: Fri, 4 Jun 2021 13:07:17 +0200 Subject: [PATCH 14/57] Remove useless comments. --- api/components/com_media/src/View/Media/JsonapiView.php | 5 ----- 1 file changed, 5 deletions(-) diff --git a/api/components/com_media/src/View/Media/JsonapiView.php b/api/components/com_media/src/View/Media/JsonapiView.php index c2aab2fc57753..96ab92b680aaf 100644 --- a/api/components/com_media/src/View/Media/JsonapiView.php +++ b/api/components/com_media/src/View/Media/JsonapiView.php @@ -99,11 +99,6 @@ protected function prepareItem($item) // Media resources have no id. $item->id = '0'; -// // Transform resource location into url. -// list('adapter' => $adapterName, 'path' => $path) = MediaHelper::adapterNameAndPath($item->path); -// $adapter = $this->getAdapter($adapterName); -// $item->path = $adapter->getUrl($path); - return $item; } } From e9c287d108b380090e34ade9c7e25beb80eda9e4 Mon Sep 17 00:00:00 2001 From: Pieter-Jan de Vries Date: Fri, 4 Jun 2021 13:18:22 +0200 Subject: [PATCH 15/57] Replace 'PATH' with 'STRING' for cleaning of input parameters. --- .../src/Controller/MediaController.php | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/api/components/com_media/src/Controller/MediaController.php b/api/components/com_media/src/Controller/MediaController.php index cd5fab18ed428..7f94eb7c17805 100644 --- a/api/components/com_media/src/Controller/MediaController.php +++ b/api/components/com_media/src/Controller/MediaController.php @@ -50,7 +50,7 @@ class MediaController extends ApiController private static $listQueryModelStateMap = [ 'path' => [ 'name' => 'path', - 'type' => 'PATH' + 'type' => 'STRING' ], 'url' => [ 'name' => 'url', @@ -69,7 +69,7 @@ class MediaController extends ApiController private static $itemQueryModelStateMap = [ 'path' => [ 'name' => 'path', - 'type' => 'PATH' + 'type' => 'STRING' ], 'url' => [ 'name' => 'url', @@ -151,7 +151,7 @@ public function displayList() // Display files in specific path. if ($this->input->exists('path')) { - $this->modelState->set('path', $this->input->get('path', '', 'PATH')); + $this->modelState->set('path', $this->input->get('path', '', 'STRING')); } // Return files (not folders) as url's. @@ -193,7 +193,7 @@ public function displayItem($path = '') $this->setModelState(self::$itemQueryModelStateMap); // Display files in specific path. - $this->modelState->set('path', $path ?: $this->input->get('path', '', 'PATH')); + $this->modelState->set('path', $path ?: $this->input->get('path', '', 'STRING')); // Return files (not folders) as url's. if ($this->input->exists('url')) @@ -239,7 +239,7 @@ private function setModelState(array $mappings) */ public function add() { - $this->modelState->set('path', $this->input->json->get('path', '', 'PATH')); + $this->modelState->set('path', $this->input->json->get('path', '', 'STRING')); // Check if an existing file may be overwritten. Defaults to false. $this->modelState->set('override', $this->input->json->get('override', false)); @@ -282,11 +282,9 @@ public function edit() throw new NotAllowed('JLIB_APPLICATION_ERROR_CREATE_RECORD_NOT_PERMITTED', 403); } - $path = $this->input->json->get('path', '', 'STRING'); - $path = $this->input->json->get('path', '', 'PATH'); - $this->modelState->set('path', $this->input->json->get('path', '', 'PATH')); + $this->modelState->set('path', $this->input->json->get('path', '', 'STRING')); // For renaming/moving files, we need the path to the existing file or folder. - $this->modelState->set('old_path', $this->input->get('path', '', 'PATH')); + $this->modelState->set('old_path', $this->input->get('path', '', 'STRING')); // Check if an existing file may be overwritten. Defaults to true. $this->modelState->set('override', $this->input->json->get('override', false)); @@ -330,7 +328,7 @@ protected function save($recordKey = null) $json = $this->input->json; // Split destination path into adapter name and file path. - ['adapter' => $adapter, 'path' => $path] = MediaHelper::adapterNameAndPath($this->input->get('path', '', 'PATH')); + ['adapter' => $adapter, 'path' => $path] = MediaHelper::adapterNameAndPath($this->input->get('path', '', 'STRING')); // Decode content, if any if ($content = base64_decode($json->get('content', '', 'raw'))) @@ -387,7 +385,7 @@ public function delete($id = null) throw new NotAllowed('JLIB_APPLICATION_ERROR_DELETE_NOT_PERMITTED', 403); } - $this->modelState->set('path', $this->input->get('path', '', 'PATH')); + $this->modelState->set('path', $this->input->get('path', '', 'STRING')); $modelName = $this->input->get('model', Inflector::singularize($this->contentType)); $model = $this->getModel($modelName, '', ['ignore_request' => true, 'state' => $this->modelState]); From 4042d7821bc976846734ff6009c5134eaec7d28a Mon Sep 17 00:00:00 2001 From: Pieter-Jan de Vries Date: Sun, 6 Jun 2021 15:34:57 +0200 Subject: [PATCH 16/57] Adds required parameter checks and removes global exception handling. --- .../src/Controller/MediaController.php | 78 +++++++++---------- 1 file changed, 35 insertions(+), 43 deletions(-) diff --git a/api/components/com_media/src/Controller/MediaController.php b/api/components/com_media/src/Controller/MediaController.php index 7f94eb7c17805..97a32d7f37f4f 100644 --- a/api/components/com_media/src/Controller/MediaController.php +++ b/api/components/com_media/src/Controller/MediaController.php @@ -12,18 +12,16 @@ \defined('_JEXEC') or die; use Joomla\CMS\Access\Exception\NotAllowed; -use Joomla\CMS\Application\Exception\NotAcceptable; use Joomla\CMS\Component\ComponentHelper; use Joomla\CMS\Filter\InputFilter; use Joomla\CMS\Language\Text; use Joomla\CMS\MVC\Controller\ApiController; -use Joomla\CMS\MVC\Controller\Exception\ResourceNotFound; use Joomla\Component\Media\Administrator\Exception\FileExistsException; -use Joomla\Component\Media\Administrator\Exception\FileNotFoundException; use Joomla\Component\Media\Administrator\Exception\InvalidPathException; use Joomla\Component\Media\Api\Helper\AdapterTrait; use Joomla\Component\Media\Api\Helper\MediaHelper; use Joomla\String\Inflector; +use Tobscure\JsonApi\Exception\InvalidParameterException; /** * Media web service controller. @@ -94,46 +92,6 @@ class MediaController extends ApiController */ protected $default_view = 'media'; - /** - * Execute a task by triggering a method in the derived class. - * This method overrides the base method, to enable mapping of com_media exceptions to API handled exceptions. - * - * @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.0 - */ - public function execute($task) - { - // Execute parent method and catch com_media specific exceptions and map them to API equivalents. - try - { - parent::execute($task); - } - // A specific file or folder was requested or meant to be updated or deleted. - catch (InvalidPathException $e) - { - throw new ResourceNotFound(); - } - // A file or folder was meant to be created, but it already exists and overwriting is not the intention. - catch (FileExistsException $e) - { - throw new NotAcceptable(); - } - // This exception is thrown when a filename - catch (FileNotFoundException $e) - { - throw new NotAcceptable(); - } - catch (\Exception $e) - { - throw $e; - } - } - /** * Display a list of files and/or folders. * @@ -239,6 +197,28 @@ private function setModelState(array $mappings) */ public function add() { + $path = $this->input->json->get('path', '', 'STRING'); + $content = $this->input->json->get('content', '', 'RAW'); + + $missingParameters = []; + + if (empty($path)) + { + $missingParameters[] = 'path'; + } + + if (empty($content)) + { + $missingParameters[] = 'content'; + } + + if (count($missingParameters)) + { + throw new InvalidParameterException( + Text::sprintf('WEBSERVICE_COM_MEDIA_MISSING_REQUIRED_PARAMETERS', implode(' & ', $missingParameters)) + ); + } + $this->modelState->set('path', $this->input->json->get('path', '', 'STRING')); // Check if an existing file may be overwritten. Defaults to false. $this->modelState->set('override', $this->input->json->get('override', false)); @@ -282,6 +262,16 @@ public function edit() throw new NotAllowed('JLIB_APPLICATION_ERROR_CREATE_RECORD_NOT_PERMITTED', 403); } + $path = $this->input->json->get('path', '', 'STRING'); + $content = $this->input->json->get('content', '', 'RAW'); + + if (empty($path) && empty($content)) + { + throw new InvalidParameterException( + Text::sprintf('WEBSERVICE_COM_MEDIA_MISSING_REQUIRED_PARAMETERS', 'path | content') + ); + } + $this->modelState->set('path', $this->input->json->get('path', '', 'STRING')); // For renaming/moving files, we need the path to the existing file or folder. $this->modelState->set('old_path', $this->input->get('path', '', 'STRING')); @@ -391,6 +381,8 @@ public function delete($id = null) $model = $this->getModel($modelName, '', ['ignore_request' => true, 'state' => $this->modelState]); $model->delete(); + + $this->app->setHeader('status', 204); } /** From 4bf9821cafbd78915669ace8e985c46c38bc7af0 Mon Sep 17 00:00:00 2001 From: Pieter-Jan de Vries Date: Sun, 6 Jun 2021 15:35:39 +0200 Subject: [PATCH 17/57] Adds media web service specific language strings. --- api/language/en-GB/com_media.ini | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 api/language/en-GB/com_media.ini diff --git a/api/language/en-GB/com_media.ini b/api/language/en-GB/com_media.ini new file mode 100644 index 0000000000000..d74fb8d95b4f4 --- /dev/null +++ b/api/language/en-GB/com_media.ini @@ -0,0 +1,11 @@ +; Joomla! Project +; (C) 2005 Open Source Matters, Inc. +; License GNU General Public License version 2 or later; see LICENSE.txt +; Note : All ini files need to be saved as UTF-8 + +WEBSERVICE_COM_MEDIA="Media web service" +WEBSERVICE_COM_MEDIA_MISSING_REQUIRED_PARAMETERS="Missing required parameter(s): %s" +WEBSERVICE_COM_MEDIA_FILE_NOT_FOUND="File not found: %s" +WEBSERVICE_COM_MEDIA_FILE_EXISTS="File exists and overwriting not requested: %s" +WEBSERVICE_COM_MEDIA_BAD_FILE_TYPE="Invalid path or file type not allowed: %s" +WEBSERVICE_COM_MEDIA_UNSUPPORTED_PARAMETER_COMBINATION="Unexpected or unsupported query parameter combination" From 5c3df9cfecee4400c37b6282ec1c340da0eab333 Mon Sep 17 00:00:00 2001 From: Pieter-Jan de Vries Date: Sun, 6 Jun 2021 15:36:23 +0200 Subject: [PATCH 18/57] Adds exception handling. --- .../com_media/src/Model/MediumModel.php | 90 +++++++++++++++---- 1 file changed, 75 insertions(+), 15 deletions(-) diff --git a/api/components/com_media/src/Model/MediumModel.php b/api/components/com_media/src/Model/MediumModel.php index 44860934dfb0b..7d68bbe4b8017 100644 --- a/api/components/com_media/src/Model/MediumModel.php +++ b/api/components/com_media/src/Model/MediumModel.php @@ -12,10 +12,14 @@ \defined('_JEXEC') or die; +use Joomla\CMS\Language\Text; +use Joomla\CMS\MVC\Controller\Exception\Save; use Joomla\CMS\MVC\Model\BaseModel; +use Joomla\Component\Media\Administrator\Exception\FileExistsException; +use Joomla\Component\Media\Administrator\Exception\FileNotFoundException; +use Joomla\Component\Media\Administrator\Exception\InvalidPathException; use Joomla\Component\Media\Administrator\Model\ApiModel; use Joomla\Component\Media\Api\Helper\MediaHelper; -use Tobscure\JsonApi\Exception\InvalidParameterException; /** * Media web service model supporting a single media item. @@ -87,9 +91,19 @@ public function save($path = NULL) { // if also requested (see below). if ($path && $oldPath) { - // ApiModel::move() (or actually LocalAdapter::move()) returns a path - // with leading slash. - $resultPath = trim($this->mediaApiModel->move($adapterName, $oldPath, $path, $override), '/'); + try + { + // ApiModel::move() (or actually LocalAdapter::move()) returns a path + // with leading slash. + $resultPath = trim($this->mediaApiModel->move($adapterName, $oldPath, $path, $override), '/'); + } + catch (FileNotFoundException $e) + { + throw new Save( + Text::sprintf('WEBSERVICE_COM_MEDIA_FILE_NOT_FOUND', $oldPath), + 404 + ); + } } // If we have a (new) path but no old path, we want to create a @@ -101,13 +115,37 @@ public function save($path = NULL) { $basename = basename($resultPath ?: $path); $dirname = dirname($resultPath ?: $path); - // If there is content, com_media's assumes the new item is a file. - // Otherwise a folder is assumed. - $name = $content - ? $this->mediaApiModel->createFile($adapterName, $basename, $dirname, $content, $override) - : $this->mediaApiModel->createFolder($adapterName, $basename, $dirname, $override); - - $resultPath = $dirname . '/' . $name; + try + { + // If there is content, com_media's assumes the new item is a file. + // Otherwise a folder is assumed. + $name = $content + ? $this->mediaApiModel->createFile($adapterName, $basename, $dirname, $content, $override) + : $this->mediaApiModel->createFolder($adapterName, $basename, $dirname, $override); + + $resultPath = $dirname . '/' . $name; + } + catch (FileNotFoundException $e) + { + throw new Save( + Text::sprintf('WEBSERVICE_COM_MEDIA_FILE_NOT_FOUND', $dirname . '/' . $basename), + 404 + ); + } + catch (FileExistsException $e) + { + throw new Save( + Text::sprintf('WEBSERVICE_COM_MEDIA_FILE_EXISTS', $dirname . '/' . $basename), + 400 + ); + } + catch (InvalidPathException $e) + { + throw new Save( + Text::sprintf('WEBSERVICE_COM_MEDIA_BAD_FILE_TYPE', $dirname . '/' . $basename), + 400 + ); + } } // If we have no (new) path but we do have an old path and we have content, @@ -119,7 +157,17 @@ public function save($path = NULL) { $basename = basename($resultPath ?: $oldPath); $dirname = dirname($resultPath ?: $oldPath); - $this->mediaApiModel->updateFile($adapterName, $basename, $dirname, $content); + try + { + $this->mediaApiModel->updateFile($adapterName, $basename, $dirname, $content); + } + catch (InvalidPathException $e) + { + throw new Save( + Text::sprintf('WEBSERVICE_COM_MEDIA_BAD_FILE_TYPE', $dirname . '/' . $basename), + 400 + ); + } $resultPath = $oldPath; } @@ -127,7 +175,10 @@ public function save($path = NULL) { // If we still have no result path, something fishy is going on. if (!$resultPath) { - throw new InvalidParameterException(); + throw new Save( + Text::_('WEBSERVICE_COM_MEDIA_UNSUPPORTED_PARAMETER_COMBINATION'), + 400 + ); } return $resultPath; @@ -148,7 +199,16 @@ public function delete() { 'path' => $path, ] = MediaHelper::adapterNameAndPath($path); - $this->mediaApiModel->delete($adapterName, $path); + try + { + $this->mediaApiModel->delete($adapterName, $path); + } + catch (FileNotFoundException $e) + { + throw new Save( + Text::sprintf('WEBSERVICE_COM_MEDIA_FILE_NOT_FOUND', $path), + 404 + ); + } } - } From 8a2dad451b8d96391e19bdde4f48485a8617dad1 Mon Sep 17 00:00:00 2001 From: Pieter-Jan de Vries Date: Mon, 7 Jun 2021 09:59:13 +0200 Subject: [PATCH 19/57] Adds missing exception handler and fixes wrong path return value after move/rename. --- api/components/com_media/src/Model/MediumModel.php | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/api/components/com_media/src/Model/MediumModel.php b/api/components/com_media/src/Model/MediumModel.php index 7d68bbe4b8017..3cc3cb52c20e2 100644 --- a/api/components/com_media/src/Model/MediumModel.php +++ b/api/components/com_media/src/Model/MediumModel.php @@ -161,6 +161,13 @@ public function save($path = NULL) { { $this->mediaApiModel->updateFile($adapterName, $basename, $dirname, $content); } + catch (FileNotFoundException $e) + { + throw new Save( + Text::sprintf('WEBSERVICE_COM_MEDIA_FILE_NOT_FOUND', $dirname . '/' . $basename), + 404 + ); + } catch (InvalidPathException $e) { throw new Save( @@ -169,7 +176,7 @@ public function save($path = NULL) { ); } - $resultPath = $oldPath; + $resultPath = $resultPath ?: $oldPath; } // If we still have no result path, something fishy is going on. From 03c413b56efa1a23b56987e2d802f8f493e5ce43 Mon Sep 17 00:00:00 2001 From: Pieter-Jan de Vries Date: Tue, 8 Jun 2021 09:18:05 +0200 Subject: [PATCH 20/57] Use exception message as error title. --- .../src/Error/JsonApi/ResourceNotFoundExceptionHandler.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/src/Error/JsonApi/ResourceNotFoundExceptionHandler.php b/libraries/src/Error/JsonApi/ResourceNotFoundExceptionHandler.php index d65a34767801d..f5ade3b3de9d0 100644 --- a/libraries/src/Error/JsonApi/ResourceNotFoundExceptionHandler.php +++ b/libraries/src/Error/JsonApi/ResourceNotFoundExceptionHandler.php @@ -47,7 +47,7 @@ public function manages(Exception $e) public function handle(Exception $e) { $status = 404; - $error = ['title' => 'Resource not found']; + $error = ['title' => $e->getMessage() ?: 'Resource not found']; $code = $e->getCode(); From d1967c5f494a1a4617ac994d84e7c3cda153ae5f Mon Sep 17 00:00:00 2001 From: Pieter-Jan de Vries Date: Tue, 8 Jun 2021 09:18:43 +0200 Subject: [PATCH 21/57] Adds proper FileNotFound exception handling for GET requests. --- .../com_media/src/Model/MediaModel.php | 17 +++- .../com_media/src/Model/MediumModel.php | 99 ++++++++++++++----- 2 files changed, 89 insertions(+), 27 deletions(-) diff --git a/api/components/com_media/src/Model/MediaModel.php b/api/components/com_media/src/Model/MediaModel.php index 34164caa1d537..d3780d4b836c4 100644 --- a/api/components/com_media/src/Model/MediaModel.php +++ b/api/components/com_media/src/Model/MediaModel.php @@ -11,9 +11,12 @@ \defined('_JEXEC') or die; +use Joomla\CMS\Language\Text; +use Joomla\CMS\MVC\Controller\Exception\ResourceNotFound; use Joomla\CMS\MVC\Model\BaseModel; use Joomla\CMS\MVC\Model\ListModelInterface; use Joomla\CMS\Pagination\Pagination; +use Joomla\Component\Media\Administrator\Exception\FileNotFoundException; use Joomla\Component\Media\Administrator\Model\ApiModel; use Joomla\Component\Media\Api\Helper\MediaHelper; @@ -62,8 +65,18 @@ public function getItems() 'content' => $this->getState('content', false) ]; - list('adapter' => $adapterName, 'path' => $path) = MediaHelper::adapterNameAndPath($this->getState('path', '')); - $files = $this->mediaApiModel->getFiles($adapterName, $path, $options); + ['adapter' => $adapterName, 'path' => $path] = MediaHelper::adapterNameAndPath($this->getState('path', '')); + try + { + $files = $this->mediaApiModel->getFiles($adapterName, $path, $options); + } + catch (FileNotFoundException $e) + { + throw new ResourceNotFound( + Text::sprintf('WEBSERVICE_COM_MEDIA_FILE_NOT_FOUND', $path), + 404 + ); + } // A hacky way to enable the standard jsonapiView::displayList() to create a Pagination object. // Because com_media's ApiModel does not support pagination as we know from regular ListModel diff --git a/api/components/com_media/src/Model/MediumModel.php b/api/components/com_media/src/Model/MediumModel.php index 3cc3cb52c20e2..bf9544d2d3783 100644 --- a/api/components/com_media/src/Model/MediumModel.php +++ b/api/components/com_media/src/Model/MediumModel.php @@ -13,6 +13,7 @@ \defined('_JEXEC') or die; use Joomla\CMS\Language\Text; +use Joomla\CMS\MVC\Controller\Exception\ResourceNotFound; use Joomla\CMS\MVC\Controller\Exception\Save; use Joomla\CMS\MVC\Model\BaseModel; use Joomla\Component\Media\Administrator\Exception\FileExistsException; @@ -26,7 +27,8 @@ * * @since 4.0 */ -class MediumModel extends BaseModel { +class MediumModel extends BaseModel +{ /** * Instance of com_media's ApiModel @@ -35,7 +37,8 @@ class MediumModel extends BaseModel { */ private $mediaApiModel; - public function __construct($config = []) { + public function __construct($config = []) + { parent::__construct($config); $this->mediaApiModel = new ApiModel(); @@ -48,20 +51,32 @@ public function __construct($config = []) { * * @since 4.0.0 */ - public function getItem() { + public function getItem() + { $options = [ 'path' => $this->getState('path', ''), - 'url' => $this->getState('url', FALSE), - 'temp' => $this->getState('temp', FALSE), - 'content' => $this->getState('content', FALSE), + 'url' => $this->getState('url', false), + 'temp' => $this->getState('temp', false), + 'content' => $this->getState('content', false), ]; [ 'adapter' => $adapterName, 'path' => $path, - ] = MediaHelper::adapterNameAndPath($this->getState('path', '')); + ] + = MediaHelper::adapterNameAndPath($this->getState('path', '')); - return $this->mediaApiModel->getFile($adapterName, $path, $options); + try + { + return $this->mediaApiModel->getFile($adapterName, $path, $options); + } + catch (FileNotFoundException $e) + { + throw new ResourceNotFound( + Text::sprintf('WEBSERVICE_COM_MEDIA_FILE_NOT_FOUND', $path), + 404 + ); + } } /** @@ -73,16 +88,18 @@ public function getItem() { * * @since 4.0.0 */ - public function save($path = NULL) { + public function save($path = null) + { $path = $this->getState('path', ''); $oldPath = $this->getState('old_path', ''); - $content = $this->getState('content', NULL); - $override = $this->getState('override', FALSE); + $content = $this->getState('content', null); + $override = $this->getState('override', false); [ 'adapter' => $adapterName, 'path' => $path, - ] = MediaHelper::adapterNameAndPath($path); + ] + = MediaHelper::adapterNameAndPath($path); $resultPath = ''; @@ -95,12 +112,18 @@ public function save($path = NULL) { { // ApiModel::move() (or actually LocalAdapter::move()) returns a path // with leading slash. - $resultPath = trim($this->mediaApiModel->move($adapterName, $oldPath, $path, $override), '/'); + $resultPath = trim( + $this->mediaApiModel->move( + $adapterName, $oldPath, $path, $override + ), '/' + ); } catch (FileNotFoundException $e) { throw new Save( - Text::sprintf('WEBSERVICE_COM_MEDIA_FILE_NOT_FOUND', $oldPath), + Text::sprintf( + 'WEBSERVICE_COM_MEDIA_FILE_NOT_FOUND', $oldPath + ), 404 ); } @@ -120,29 +143,42 @@ public function save($path = NULL) { // If there is content, com_media's assumes the new item is a file. // Otherwise a folder is assumed. $name = $content - ? $this->mediaApiModel->createFile($adapterName, $basename, $dirname, $content, $override) - : $this->mediaApiModel->createFolder($adapterName, $basename, $dirname, $override); + ? $this->mediaApiModel->createFile( + $adapterName, $basename, $dirname, $content, $override + ) + : $this->mediaApiModel->createFolder( + $adapterName, $basename, $dirname, $override + ); $resultPath = $dirname . '/' . $name; } catch (FileNotFoundException $e) { throw new Save( - Text::sprintf('WEBSERVICE_COM_MEDIA_FILE_NOT_FOUND', $dirname . '/' . $basename), + Text::sprintf( + 'WEBSERVICE_COM_MEDIA_FILE_NOT_FOUND', + $dirname . '/' . $basename + ), 404 ); } catch (FileExistsException $e) { throw new Save( - Text::sprintf('WEBSERVICE_COM_MEDIA_FILE_EXISTS', $dirname . '/' . $basename), + Text::sprintf( + 'WEBSERVICE_COM_MEDIA_FILE_EXISTS', + $dirname . '/' . $basename + ), 400 ); } catch (InvalidPathException $e) { throw new Save( - Text::sprintf('WEBSERVICE_COM_MEDIA_BAD_FILE_TYPE', $dirname . '/' . $basename), + Text::sprintf( + 'WEBSERVICE_COM_MEDIA_BAD_FILE_TYPE', + $dirname . '/' . $basename + ), 400 ); } @@ -159,19 +195,27 @@ public function save($path = NULL) { try { - $this->mediaApiModel->updateFile($adapterName, $basename, $dirname, $content); + $this->mediaApiModel->updateFile( + $adapterName, $basename, $dirname, $content + ); } catch (FileNotFoundException $e) { throw new Save( - Text::sprintf('WEBSERVICE_COM_MEDIA_FILE_NOT_FOUND', $dirname . '/' . $basename), + Text::sprintf( + 'WEBSERVICE_COM_MEDIA_FILE_NOT_FOUND', + $dirname . '/' . $basename + ), 404 ); } catch (InvalidPathException $e) { throw new Save( - Text::sprintf('WEBSERVICE_COM_MEDIA_BAD_FILE_TYPE', $dirname . '/' . $basename), + Text::sprintf( + 'WEBSERVICE_COM_MEDIA_BAD_FILE_TYPE', + $dirname . '/' . $basename + ), 400 ); } @@ -183,7 +227,9 @@ public function save($path = NULL) { if (!$resultPath) { throw new Save( - Text::_('WEBSERVICE_COM_MEDIA_UNSUPPORTED_PARAMETER_COMBINATION'), + Text::_( + 'WEBSERVICE_COM_MEDIA_UNSUPPORTED_PARAMETER_COMBINATION' + ), 400 ); } @@ -198,13 +244,15 @@ public function save($path = NULL) { * * @since 4.0.0 */ - public function delete() { + public function delete() + { $path = $this->getState('path', ''); [ 'adapter' => $adapterName, 'path' => $path, - ] = MediaHelper::adapterNameAndPath($path); + ] + = MediaHelper::adapterNameAndPath($path); try { @@ -218,4 +266,5 @@ public function delete() { ); } } + } From 07464a7ed787f17fcec8da60e462d47a80f3a0bf Mon Sep 17 00:00:00 2001 From: Pieter-Jan de Vries Date: Thu, 10 Jun 2021 10:21:47 +0200 Subject: [PATCH 22/57] Fixes handling of status code. --- libraries/src/Error/JsonApi/SaveExceptionHandler.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/libraries/src/Error/JsonApi/SaveExceptionHandler.php b/libraries/src/Error/JsonApi/SaveExceptionHandler.php index fe76941e55e21..a30cab0f68de1 100644 --- a/libraries/src/Error/JsonApi/SaveExceptionHandler.php +++ b/libraries/src/Error/JsonApi/SaveExceptionHandler.php @@ -53,7 +53,10 @@ public function handle(Exception $e) $status = $e->getCode(); } - $error = ['title' => $e->getMessage()]; + $error = [ + 'title' => $e->getMessage(), + 'code' => $status + ]; return new ResponseBag($status, [$error]); } From c4d1d90db8b4f2e8cd021f9ad52fc1410974a20d Mon Sep 17 00:00:00 2001 From: Pieter-Jan de Vries Date: Thu, 10 Jun 2021 10:41:12 +0200 Subject: [PATCH 23/57] Fixes some types, formatting and similar stuff. --- .../language/en-GB/plg_webservices_media.ini | 2 +- .../src/Controller/MediaController.php | 30 +++++++++---------- .../com_media/src/Helper/AdapterTrait.php | 6 ++-- .../com_media/src/Helper/MediaHelper.php | 12 ++++---- .../com_media/src/Model/MediaModel.php | 12 ++++---- .../com_media/src/Model/MediumModel.php | 8 ++--- .../com_media/src/View/Media/JsonapiView.php | 10 +++---- plugins/webservices/media/media.php | 6 ++-- 8 files changed, 43 insertions(+), 43 deletions(-) diff --git a/administrator/language/en-GB/plg_webservices_media.ini b/administrator/language/en-GB/plg_webservices_media.ini index ec0a32f2a204f..343ffabc7f171 100644 --- a/administrator/language/en-GB/plg_webservices_media.ini +++ b/administrator/language/en-GB/plg_webservices_media.ini @@ -1,5 +1,5 @@ ; Joomla! Project -; (C) 20211 Open Source Matters, Inc. +; (C) 20221 Open Source Matters, Inc. ; License GNU General Public License version 2 or later; see LICENSE.txt ; Note : All ini files need to be saved as UTF-8 diff --git a/api/components/com_media/src/Controller/MediaController.php b/api/components/com_media/src/Controller/MediaController.php index 97a32d7f37f4f..48f7a7d26e0c5 100644 --- a/api/components/com_media/src/Controller/MediaController.php +++ b/api/components/com_media/src/Controller/MediaController.php @@ -26,7 +26,7 @@ /** * Media web service controller. * - * @since 4.0.0 + * @since __DEPLOY_VERSION__ */ class MediaController extends ApiController { @@ -36,7 +36,7 @@ class MediaController extends ApiController * The content type of the item. * * @var string - * @since 4.0.0 + * @since __DEPLOY_VERSION__ */ protected $contentType = 'media'; @@ -88,7 +88,7 @@ class MediaController extends ApiController * * @var string * - * @since 3.0 + * @since __DEPLOY_VERSION__ */ protected $default_view = 'media'; @@ -99,7 +99,7 @@ class MediaController extends ApiController * * @throws \Exception * - * @since 4.0.0 + * @since __DEPLOY_VERSION__ */ public function displayList() { @@ -143,7 +143,7 @@ public function displayList() * @throws InvalidPathException * @throws \Exception * - * @since 4.0.0 + * @since __DEPLOY_VERSION__ */ public function displayItem($path = '') { @@ -169,7 +169,7 @@ public function displayItem($path = '') * * @return void * - * @since 4.0.0 + * @since __DEPLOY_VERSION__ */ private function setModelState(array $mappings) { @@ -192,8 +192,8 @@ private function setModelState(array $mappings) * @throws \RuntimeException * @throws \Exception * - * @since 4.0.0 - * @since 4.0.0 + * @since __DEPLOY_VERSION__ + * @since __DEPLOY_VERSION__ */ public function add() { @@ -233,7 +233,7 @@ public function add() * * @return boolean * - * @since 4.0.0 + * @since __DEPLOY_VERSION__ */ protected function allowAdd($data = array()) { @@ -252,7 +252,7 @@ protected function allowAdd($data = array()) * @throws \RuntimeException * @throws \Exception * - * @since 4.0.0 + * @since __DEPLOY_VERSION__ */ public function edit() { @@ -290,7 +290,7 @@ public function edit() * * @return boolean * - * @since 4.0.0 + * @since __DEPLOY_VERSION__ */ protected function allowEdit($data = array(), $key = 'id') { @@ -307,7 +307,7 @@ protected function allowEdit($data = array(), $key = 'id') * * @return integer The record ID on success, false on failure * - * @since 4.0.0 + * @since __DEPLOY_VERSION__ */ protected function save($recordKey = null) { @@ -339,7 +339,7 @@ protected function save($recordKey = null) * * @throws \RuntimeException * - * @since 4.0.0 + * @since __DEPLOY_VERSION__ */ private function checkContent() { @@ -366,7 +366,7 @@ private function checkContent() * @throws \RuntimeException * @throws \Exception * - * @since 4.0.0 + * @since __DEPLOY_VERSION__ */ public function delete($id = null) { @@ -390,7 +390,7 @@ public function delete($id = null) * * @return boolean * - * @since 4.0.0 + * @since __DEPLOY_VERSION__ */ protected function allowDelete() { diff --git a/api/components/com_media/src/Helper/AdapterTrait.php b/api/components/com_media/src/Helper/AdapterTrait.php index 192e1ba582088..f041ef9c1f640 100644 --- a/api/components/com_media/src/Helper/AdapterTrait.php +++ b/api/components/com_media/src/Helper/AdapterTrait.php @@ -20,7 +20,7 @@ /** * Trait for classes that need adapters. * - * @since 4.0.0 + * @since __DEPLOY_VERSION__ */ trait AdapterTrait { @@ -29,7 +29,7 @@ trait AdapterTrait * * @var ProviderManager * - * @since 4.0.0 + * @since __DEPLOY_VERSION__ */ private $providerManager = null; @@ -40,7 +40,7 @@ trait AdapterTrait * * @throws \Exception * - * @since 4.0.0 + * @since __DEPLOY_VERSION__ */ private function getAdapter(String $name) { diff --git a/api/components/com_media/src/Helper/MediaHelper.php b/api/components/com_media/src/Helper/MediaHelper.php index d06e02d6d0813..5e1f61482b6e0 100644 --- a/api/components/com_media/src/Helper/MediaHelper.php +++ b/api/components/com_media/src/Helper/MediaHelper.php @@ -9,14 +9,14 @@ namespace Joomla\Component\Media\Api\Helper; -use Joomla\CMS\Component\ComponentHelper; - \defined('_JEXEC') or die; +use Joomla\CMS\Component\ComponentHelper; + /** * Helper methods for media web service. * - * @since 4.0.0 + * @since __DEPLOY_VERSION__ */ class MediaHelper { @@ -27,14 +27,14 @@ class MediaHelper * * @return array An array with elements 'adapter' and 'path'. * - * @since 4.0 + * @since __DEPLOY_VERSION__ */ public static function adapterNameAndPath(String $path) { $result = []; $parts = explode(':', $path, 2); - // If we have 2 parts, we have both an adapetr name and a file path. + // If we have 2 parts, we have both an adapter name and a file path. if (count($parts) == 2) { $result['adapter'] = $parts[0]; @@ -43,7 +43,7 @@ public static function adapterNameAndPath(String $path) return $result; } - // If we have less than 2 parts, we return a default aadapter name. + // If we have less than 2 parts, we return a default adapter name. $result['adapter'] = self::defaultAdapterName(); // If we have 1 part, we return it as the path. Otherwise we return a default path. $result['path'] = count($parts) ? $parts[0] : '/'; diff --git a/api/components/com_media/src/Model/MediaModel.php b/api/components/com_media/src/Model/MediaModel.php index d3780d4b836c4..6d8411f642b7a 100644 --- a/api/components/com_media/src/Model/MediaModel.php +++ b/api/components/com_media/src/Model/MediaModel.php @@ -23,7 +23,7 @@ /** * Media web service model supporting lists of media items. * - * @since 4.0 + * @since __DEPLOY_VERSION__ */ class MediaModel extends BaseModel implements ListModelInterface { @@ -52,7 +52,7 @@ public function __construct($config = []) * * @return array An array of data items. * - * @since 4.0.0 + * @since __DEPLOY_VERSION__ */ public function getItems() { @@ -68,7 +68,7 @@ public function getItems() ['adapter' => $adapterName, 'path' => $path] = MediaHelper::adapterNameAndPath($this->getState('path', '')); try { - $files = $this->mediaApiModel->getFiles($adapterName, $path, $options); + $files = $this->mediaApiModel->getFiles($adapterName, $path, $options); } catch (FileNotFoundException $e) { @@ -91,7 +91,7 @@ public function getItems() * * @return Pagination A Pagination object for the data set. * - * @since 4.0 + * @since __DEPLOY_VERSION__ */ public function getPagination() { @@ -105,7 +105,7 @@ public function getPagination() * * @return integer The starting number of items available in the data set. * - * @since 4.0 + * @since __DEPLOY_VERSION__ */ public function getStart() { @@ -117,7 +117,7 @@ public function getStart() * * @return integer The total number of items available in the data set. * - * @since 1.6 + * @since __DEPLOY_VERSION__ */ public function getTotal() { diff --git a/api/components/com_media/src/Model/MediumModel.php b/api/components/com_media/src/Model/MediumModel.php index bf9544d2d3783..b1390d6dfae9c 100644 --- a/api/components/com_media/src/Model/MediumModel.php +++ b/api/components/com_media/src/Model/MediumModel.php @@ -25,7 +25,7 @@ /** * Media web service model supporting a single media item. * - * @since 4.0 + * @since __DEPLOY_VERSION__ */ class MediumModel extends BaseModel { @@ -49,7 +49,7 @@ public function __construct($config = []) * * @return \stdClass A file or folder object. * - * @since 4.0.0 + * @since __DEPLOY_VERSION__ */ public function getItem() { @@ -86,7 +86,7 @@ public function getItem() * * @return integer The record ID on success, false on failure * - * @since 4.0.0 + * @since __DEPLOY_VERSION__ */ public function save($path = null) { @@ -242,7 +242,7 @@ public function save($path = null) * * @return void * - * @since 4.0.0 + * @since __DEPLOY_VERSION__ */ public function delete() { diff --git a/api/components/com_media/src/View/Media/JsonapiView.php b/api/components/com_media/src/View/Media/JsonapiView.php index 96ab92b680aaf..41a9c6c82819e 100644 --- a/api/components/com_media/src/View/Media/JsonapiView.php +++ b/api/components/com_media/src/View/Media/JsonapiView.php @@ -19,7 +19,7 @@ /** * Media web service view * - * @since 4.0.0 + * @since __DEPLOY_VERSION__ */ class JsonapiView extends BaseApiView { @@ -29,7 +29,7 @@ class JsonapiView extends BaseApiView * The fields to render item in the documents * * @var array - * @since 4.0.0 + * @since __DEPLOY_VERSION__ */ protected $fieldsToRenderItem = [ 'type', @@ -55,7 +55,7 @@ class JsonapiView extends BaseApiView * The fields to render items in the documents * * @var array - * @since 4.0.0 + * @since __DEPLOY_VERSION__ */ protected $fieldsToRenderList = [ 'type', @@ -81,7 +81,7 @@ class JsonapiView extends BaseApiView * Holds the available media file adapters. * * @var ProviderManager - * @since 4.0.0 + * @since __DEPLOY_VERSION__ */ private $providerManager = null; @@ -92,7 +92,7 @@ class JsonapiView extends BaseApiView * * @return object * - * @since 4.0.0 + * @since __DEPLOY_VERSION__ */ protected function prepareItem($item) { diff --git a/plugins/webservices/media/media.php b/plugins/webservices/media/media.php index 14be21b996255..38b8cf485ae1a 100644 --- a/plugins/webservices/media/media.php +++ b/plugins/webservices/media/media.php @@ -16,7 +16,7 @@ /** * Web Services adapter for com_media. * - * @since 4.0.0 + * @since __DEPLOY_VERSION__ */ class PlgWebservicesMedia extends CMSPlugin { @@ -24,7 +24,7 @@ class PlgWebservicesMedia extends CMSPlugin * Load the language file on instantiation. * * @var boolean - * @since 4.0.0 + * @since __DEPLOY_VERSION__ */ protected $autoloadLanguage = true; @@ -35,7 +35,7 @@ class PlgWebservicesMedia extends CMSPlugin * * @return void * - * @since 4.0.0 + * @since __DEPLOY_VERSION__ */ public function onBeforeApiRoute(&$router) { From 757f43984d1941464ae898db5caeffe33cdfb7fb Mon Sep 17 00:00:00 2001 From: Pieter-Jan de Vries Date: Thu, 10 Jun 2021 16:33:12 +0200 Subject: [PATCH 24/57] Fixes yet another sloppy typo. --- administrator/language/en-GB/plg_webservices_media.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/administrator/language/en-GB/plg_webservices_media.ini b/administrator/language/en-GB/plg_webservices_media.ini index 343ffabc7f171..b2b25bba1114b 100644 --- a/administrator/language/en-GB/plg_webservices_media.ini +++ b/administrator/language/en-GB/plg_webservices_media.ini @@ -1,5 +1,5 @@ ; Joomla! Project -; (C) 20221 Open Source Matters, Inc. +; (C) 2021 Open Source Matters, Inc. ; License GNU General Public License version 2 or later; see LICENSE.txt ; Note : All ini files need to be saved as UTF-8 From 4d816ce3b230e8f49f72b80f8ed05418649db5f2 Mon Sep 17 00:00:00 2001 From: Allon Moritz Date: Wed, 29 Sep 2021 09:55:23 +0200 Subject: [PATCH 25/57] restore file --- .../language/en-GB/plg_webservices_content.sys.ini | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 administrator/language/en-GB/plg_webservices_content.sys.ini diff --git a/administrator/language/en-GB/plg_webservices_content.sys.ini b/administrator/language/en-GB/plg_webservices_content.sys.ini new file mode 100644 index 0000000000000..6202874af339f --- /dev/null +++ b/administrator/language/en-GB/plg_webservices_content.sys.ini @@ -0,0 +1,7 @@ +; Joomla! Project +; (C) 2019 Open Source Matters, Inc. +; License GNU General Public License version 2 or later; see LICENSE.txt +; Note : All ini files need to be saved as UTF-8 + +PLG_WEBSERVICES_CONTENT="Web Services - Content" +PLG_WEBSERVICES_CONTENT_XML_DESCRIPTION="Add article routes to the API for your website." From 83fc1304e715999739e554093760174732b4c9cf Mon Sep 17 00:00:00 2001 From: Allon Moritz Date: Thu, 7 Oct 2021 13:33:33 +0200 Subject: [PATCH 26/57] Fix some docs --- .../src/Controller/MediaController.php | 43 +++++++++++-------- .../com_media/src/Helper/AdapterTrait.php | 6 +-- .../com_media/src/Helper/MediaHelper.php | 16 +++++-- .../com_media/src/Model/MediaModel.php | 30 +++++++------ .../com_media/src/Model/MediumModel.php | 33 +++++++------- .../com_media/src/View/Media/JsonapiView.php | 7 ++- api/language/en-GB/com_media.ini | 2 +- .../ResourceNotFoundExceptionHandler.php | 2 +- plugins/webservices/media/media.php | 17 +++++++- plugins/webservices/media/media.xml | 2 +- 10 files changed, 96 insertions(+), 62 deletions(-) diff --git a/api/components/com_media/src/Controller/MediaController.php b/api/components/com_media/src/Controller/MediaController.php index 48f7a7d26e0c5..daab209882a44 100644 --- a/api/components/com_media/src/Controller/MediaController.php +++ b/api/components/com_media/src/Controller/MediaController.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\Component\Media\Api\Controller; @@ -44,6 +44,7 @@ class MediaController extends ApiController * Query parameters => model state mappings * * @var array + * @since __DEPLOY_VERSION__ */ private static $listQueryModelStateMap = [ 'path' => [ @@ -64,6 +65,12 @@ class MediaController extends ApiController ], ]; + /** + * Item query parameters => model state mappings + * + * @var array + * @since __DEPLOY_VERSION__ + */ private static $itemQueryModelStateMap = [ 'path' => [ 'name' => 'path', @@ -112,7 +119,7 @@ public function displayList() $this->modelState->set('path', $this->input->get('path', '', 'STRING')); } - // Return files (not folders) as url's. + // Return files (not folders) as urls. if ($this->input->exists('url')) { $this->modelState->set('url', $this->input->get('url', true, 'BOOLEAN')); @@ -153,7 +160,7 @@ public function displayItem($path = '') // Display files in specific path. $this->modelState->set('path', $path ?: $this->input->get('path', '', 'STRING')); - // Return files (not folders) as url's. + // Return files (not folders) as urls. if ($this->input->exists('url')) { $this->modelState->set('url', $this->input->get('url', true, 'BOOLEAN')); @@ -171,7 +178,7 @@ public function displayItem($path = '') * * @since __DEPLOY_VERSION__ */ - private function setModelState(array $mappings) + private function setModelState(array $mappings): void { foreach ($mappings as $queryName => $modelState) { @@ -189,13 +196,13 @@ private function setModelState(array $mappings) * * @throws FileExistsException * @throws InvalidPathException + * @throws InvalidParameterException * @throws \RuntimeException * @throws \Exception * * @since __DEPLOY_VERSION__ - * @since __DEPLOY_VERSION__ */ - public function add() + public function add(): void { $path = $this->input->json->get('path', '', 'STRING'); $content = $this->input->json->get('content', '', 'RAW'); @@ -212,7 +219,7 @@ public function add() $missingParameters[] = 'content'; } - if (count($missingParameters)) + if (\count($missingParameters)) { throw new InvalidParameterException( Text::sprintf('WEBSERVICE_COM_MEDIA_MISSING_REQUIRED_PARAMETERS', implode(' & ', $missingParameters)) @@ -235,7 +242,7 @@ public function add() * * @since __DEPLOY_VERSION__ */ - protected function allowAdd($data = array()) + protected function allowAdd($data = array()): bool { $user = $this->app->getIdentity(); @@ -254,7 +261,7 @@ protected function allowAdd($data = array()) * * @since __DEPLOY_VERSION__ */ - public function edit() + public function edit(): void { // Access check. if (!$this->allowEdit()) @@ -292,12 +299,12 @@ public function edit() * * @since __DEPLOY_VERSION__ */ - protected function allowEdit($data = array(), $key = 'id') + protected function allowEdit($data = array(), $key = 'id'): bool { $user = $this->app->getIdentity(); // com_media's access rules contains no specific update rule. - return $user->authorise('core.create', 'com_media'); + return $user->authorise('core.edit', 'com_media'); } /** @@ -309,7 +316,7 @@ protected function allowEdit($data = array(), $key = 'id') * * @since __DEPLOY_VERSION__ */ - protected function save($recordKey = null) + protected function save($recordKey = null): int { // Explicitly get the single item model name. $modelName = $this->input->get('model', Inflector::singularize($this->contentType)); @@ -326,7 +333,7 @@ protected function save($recordKey = null) $this->checkContent(); } - // If there is no content, com_media's assumes the path refers to a folder. + // If there is no content, com_media assumes the path refers to a folder. $this->modelState->set('content', $content); return $model->save(); @@ -341,7 +348,7 @@ protected function save($recordKey = null) * * @since __DEPLOY_VERSION__ */ - private function checkContent() + private function checkContent(): void { $params = ComponentHelper::getParams('com_media'); $helper = new \Joomla\CMS\Helper\MediaHelper(); @@ -368,7 +375,7 @@ private function checkContent() * * @since __DEPLOY_VERSION__ */ - public function delete($id = null) + public function delete($id = null): void { if (!$this->allowDelete()) { @@ -392,7 +399,7 @@ public function delete($id = null) * * @since __DEPLOY_VERSION__ */ - protected function allowDelete() + protected function allowDelete(): bool { $user = $this->app->getIdentity(); diff --git a/api/components/com_media/src/Helper/AdapterTrait.php b/api/components/com_media/src/Helper/AdapterTrait.php index f041ef9c1f640..233d393469b5e 100644 --- a/api/components/com_media/src/Helper/AdapterTrait.php +++ b/api/components/com_media/src/Helper/AdapterTrait.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\Component\Media\Api\Helper; diff --git a/api/components/com_media/src/Helper/MediaHelper.php b/api/components/com_media/src/Helper/MediaHelper.php index 5e1f61482b6e0..dbd852e8ea73b 100644 --- a/api/components/com_media/src/Helper/MediaHelper.php +++ b/api/components/com_media/src/Helper/MediaHelper.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\Component\Media\Api\Helper; @@ -45,13 +45,21 @@ public static function adapterNameAndPath(String $path) // If we have less than 2 parts, we return a default adapter name. $result['adapter'] = self::defaultAdapterName(); + // 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; } - private static function defaultAdapterName() + /** + * Returns the default adapter name. + * + * @return string The adapter name + * + * @since __DEPLOY_VERSION__ + */ + private static function defaultAdapterName(): string { static $comMediaParams; diff --git a/api/components/com_media/src/Model/MediaModel.php b/api/components/com_media/src/Model/MediaModel.php index 6d8411f642b7a..ce94e0162e081 100644 --- a/api/components/com_media/src/Model/MediaModel.php +++ b/api/components/com_media/src/Model/MediaModel.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\Component\Media\Api\Model; @@ -31,12 +31,16 @@ class MediaModel extends BaseModel implements ListModelInterface * Instance of com_media's ApiModel * * @var ApiModel + * @since __DEPLOY_VERSION__ */ private $mediaApiModel; - /* + /** * A hacky way to enable the standard jsonapiView::displayList() to create a Pagination object, * since com_media's ApiModel does not support pagination as we know from regular ListModel derived models. + * + * @var int + * @since __DEPLOY_VERSION__ */ private $total = 0; @@ -54,7 +58,7 @@ public function __construct($config = []) * * @since __DEPLOY_VERSION__ */ - public function getItems() + public function getItems(): array { // Map web service model state to com_media options. $options = [ @@ -78,9 +82,11 @@ public function getItems() ); } - // A hacky way to enable the standard jsonapiView::displayList() to create a Pagination object. - // Because com_media's ApiModel does not support pagination as we know from regular ListModel - // derived models, we always return all retrieved items. + /* + * A hacky way to enable the standard jsonapiView::displayList() to create a Pagination object. + * Because com_media's ApiModel does not support pagination as we know from regular ListModel + * derived models, we always return all retrieved items. + */ $this->total = count($files); return $files; @@ -93,9 +99,9 @@ public function getItems() * * @since __DEPLOY_VERSION__ */ - public function getPagination() + public function getPagination(): Pagination { - return new Pagination($this->getTotal(), $this->getStart(), 0);; + return new Pagination($this->getTotal(), $this->getStart(), 0); } /** @@ -107,7 +113,7 @@ public function getPagination() * * @since __DEPLOY_VERSION__ */ - public function getStart() + public function getStart(): int { return 0; } @@ -119,7 +125,7 @@ public function getStart() * * @since __DEPLOY_VERSION__ */ - public function getTotal() + public function getTotal(): int { return $this->total; } diff --git a/api/components/com_media/src/Model/MediumModel.php b/api/components/com_media/src/Model/MediumModel.php index b1390d6dfae9c..0681f6ecc898e 100644 --- a/api/components/com_media/src/Model/MediumModel.php +++ b/api/components/com_media/src/Model/MediumModel.php @@ -1,11 +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\Component\Media\Api\Model; @@ -29,11 +28,11 @@ */ class MediumModel extends BaseModel { - /** * Instance of com_media's ApiModel * * @var ApiModel + * @since __DEPLOY_VERSION__ */ private $mediaApiModel; @@ -50,6 +49,7 @@ public function __construct($config = []) * @return \stdClass A file or folder object. * * @since __DEPLOY_VERSION__ + * @throws ResourceNotFound */ public function getItem() { @@ -87,8 +87,9 @@ public function getItem() * @return integer The record ID on success, false on failure * * @since __DEPLOY_VERSION__ + * @throws Save */ - public function save($path = null) + public function save($path = null): int { $path = $this->getState('path', ''); $oldPath = $this->getState('old_path', ''); @@ -103,19 +104,19 @@ public function save($path = null) $resultPath = ''; - // If we have a (new) path and an old path, we want to move an existing - // file or folder. This must be done before updating the content of a file, - // if also requested (see below). + /* + * If we have a (new) path and an old path, we want to move an existing + * file or folder. This must be done before updating the content of a file, + * if also requested (see below). + */ if ($path && $oldPath) { try { - // ApiModel::move() (or actually LocalAdapter::move()) returns a path - // with leading slash. + // ApiModel::move() (or actually LocalAdapter::move()) returns a path with leading slash. $resultPath = trim( - $this->mediaApiModel->move( - $adapterName, $oldPath, $path, $override - ), '/' + $this->mediaApiModel->move($adapterName, $oldPath, $path, $override), + '/' ); } catch (FileNotFoundException $e) @@ -243,8 +244,9 @@ public function save($path = null) * @return void * * @since __DEPLOY_VERSION__ + * @throws Save */ - public function delete() + public function delete(): void { $path = $this->getState('path', ''); @@ -266,5 +268,4 @@ public function delete() ); } } - } diff --git a/api/components/com_media/src/View/Media/JsonapiView.php b/api/components/com_media/src/View/Media/JsonapiView.php index 41a9c6c82819e..fd4d861417242 100644 --- a/api/components/com_media/src/View/Media/JsonapiView.php +++ b/api/components/com_media/src/View/Media/JsonapiView.php @@ -14,7 +14,6 @@ use Joomla\CMS\MVC\View\JsonApiView as BaseApiView; use Joomla\Component\Media\Administrator\Provider\ProviderManager; use Joomla\Component\Media\Api\Helper\AdapterTrait; -use Joomla\Component\Media\Api\Helper\MediaHelper; /** * Media web service view @@ -28,7 +27,7 @@ class JsonapiView extends BaseApiView /** * The fields to render item in the documents * - * @var array + * @var array * @since __DEPLOY_VERSION__ */ protected $fieldsToRenderItem = [ @@ -54,7 +53,7 @@ class JsonapiView extends BaseApiView /** * The fields to render items in the documents * - * @var array + * @var array * @since __DEPLOY_VERSION__ */ protected $fieldsToRenderList = [ @@ -80,7 +79,7 @@ class JsonapiView extends BaseApiView /** * Holds the available media file adapters. * - * @var ProviderManager + * @var ProviderManager|null * @since __DEPLOY_VERSION__ */ private $providerManager = null; diff --git a/api/language/en-GB/com_media.ini b/api/language/en-GB/com_media.ini index d74fb8d95b4f4..3e36202c201d7 100644 --- a/api/language/en-GB/com_media.ini +++ b/api/language/en-GB/com_media.ini @@ -1,5 +1,5 @@ ; Joomla! Project -; (C) 2005 Open Source Matters, Inc. +; (C) 2021 Open Source Matters, Inc. ; License GNU General Public License version 2 or later; see LICENSE.txt ; Note : All ini files need to be saved as UTF-8 diff --git a/libraries/src/Error/JsonApi/ResourceNotFoundExceptionHandler.php b/libraries/src/Error/JsonApi/ResourceNotFoundExceptionHandler.php index f5ade3b3de9d0..d65a34767801d 100644 --- a/libraries/src/Error/JsonApi/ResourceNotFoundExceptionHandler.php +++ b/libraries/src/Error/JsonApi/ResourceNotFoundExceptionHandler.php @@ -47,7 +47,7 @@ public function manages(Exception $e) public function handle(Exception $e) { $status = 404; - $error = ['title' => $e->getMessage() ?: 'Resource not found']; + $error = ['title' => 'Resource not found']; $code = $e->getCode(); diff --git a/plugins/webservices/media/media.php b/plugins/webservices/media/media.php index 38b8cf485ae1a..d64baa88595ad 100644 --- a/plugins/webservices/media/media.php +++ b/plugins/webservices/media/media.php @@ -37,7 +37,7 @@ class PlgWebservicesMedia extends CMSPlugin * * @since __DEPLOY_VERSION__ */ - public function onBeforeApiRoute(&$router) + public function onBeforeApiRoute(&$router): void { $this->createCRUDRoutes( $router, @@ -47,7 +47,20 @@ public function onBeforeApiRoute(&$router) ); } - private function createCRUDRoutes(&$router, $baseName, $controller, $defaults = [], $publicGets = false) + /** + * Creates CRUD routes. + * + * @param ApiRouter &$router The API Routing object + * @param string $baseName The base name of the component. + * @param string $controller The name of the controller that contains CRUD functions. + * @param array $defaults An array of default values that are used when the URL is matched. + * @param bool $publicGets Allow the public to make GET requests. + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + private function createCRUDRoutes(&$router, $baseName, $controller, $defaults = [], $publicGets = false): void { $getDefaults = array_merge(['public' => $publicGets], $defaults); diff --git a/plugins/webservices/media/media.xml b/plugins/webservices/media/media.xml index 6184eab57f736..95574782634dd 100644 --- a/plugins/webservices/media/media.xml +++ b/plugins/webservices/media/media.xml @@ -7,7 +7,7 @@ GNU General Public License version 2 or later; see LICENSE.txt admin@joomla.org www.joomla.org - 4.0.0 + __DEPLOY_VERSION__ PLG_WEBSERVICES_MEDIA_XML_DESCRIPTION media.php From 4075761e17d0a1c320e7fcfdb44b00265bf791bd Mon Sep 17 00:00:00 2001 From: Allon Moritz Date: Thu, 7 Oct 2021 15:07:29 +0200 Subject: [PATCH 27/57] Adapter endpoint --- .../src/Controller/AdaptersController.php | 63 ++++++++++ .../com_media/src/Model/AdapterModel.php | 62 ++++++++++ .../com_media/src/Model/AdaptersModel.php | 113 ++++++++++++++++++ .../src/View/Adapters/JsonapiView.php | 49 ++++++++ plugins/webservices/media/media.php | 41 ++++++- 5 files changed, 323 insertions(+), 5 deletions(-) create mode 100644 api/components/com_media/src/Controller/AdaptersController.php create mode 100644 api/components/com_media/src/Model/AdapterModel.php create mode 100644 api/components/com_media/src/Model/AdaptersModel.php create mode 100644 api/components/com_media/src/View/Adapters/JsonapiView.php diff --git a/api/components/com_media/src/Controller/AdaptersController.php b/api/components/com_media/src/Controller/AdaptersController.php new file mode 100644 index 0000000000000..c8c23df127dc6 --- /dev/null +++ b/api/components/com_media/src/Controller/AdaptersController.php @@ -0,0 +1,63 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\Component\Media\Api\Controller; + +\defined('_JEXEC') or die; + +use Joomla\CMS\MVC\Controller\ApiController; +use Joomla\Component\Media\Administrator\Exception\InvalidPathException; +use Joomla\Component\Media\Api\Helper\AdapterTrait; + +/** + * Media web service controller. + * + * @since __DEPLOY_VERSION__ + */ +class AdaptersController extends ApiController +{ + use AdapterTrait; + + /** + * The content type of the item. + * + * @var string + * @since __DEPLOY_VERSION__ + */ + protected $contentType = 'adapters'; + + /** + * The default view for the display method. + * + * @var string + * + * @since __DEPLOY_VERSION__ + */ + protected $default_view = 'adapters'; + + /** + * Display one specific adapter. + * + * @param string $path The path of the file to display. Leave empty if you want to retrieve data from the request. + * + * @return static A \JControllerLegacy object to support chaining. + * + * @throws InvalidPathException + * @throws \Exception + * + * @since __DEPLOY_VERSION__ + */ + public function displayItem($path = '') + { + // Set the id as the parent sets it as int + $this->modelState->set('id', $this->input->get('id', '', 'string')); + + return parent::displayItem(); + } +} diff --git a/api/components/com_media/src/Model/AdapterModel.php b/api/components/com_media/src/Model/AdapterModel.php new file mode 100644 index 0000000000000..7361c32090396 --- /dev/null +++ b/api/components/com_media/src/Model/AdapterModel.php @@ -0,0 +1,62 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\Component\Media\Api\Model; + +\defined('_JEXEC') or die; + +use Joomla\CMS\Factory; +use Joomla\CMS\MVC\Model\BaseModel; +use Joomla\CMS\Plugin\PluginHelper; +use Joomla\Component\Media\Administrator\Event\MediaProviderEvent; +use Joomla\Component\Media\Administrator\Provider\ProviderManager; + +/** + * Media web service model supporting a single adapter item. + * + * @since __DEPLOY_VERSION__ + */ +class AdapterModel extends BaseModel +{ + /** + * Method to get a single adapter. + * + * @return \stdClass The adapter. + * + * @since __DEPLOY_VERSION__ + */ + public function getItem(): \stdClass + { + $providerManager = new ProviderManager; + + // Fire the event to get the results + $eventParameters = ['context' => 'AdapterManager', 'providerManager' => $providerManager]; + $event = new MediaProviderEvent('onSetupProviders', $eventParameters); + PluginHelper::importPlugin('filesystem'); + Factory::getApplication()->getDispatcher()->dispatch('onSetupProviders', $event); + + list($provider, $account) = array_pad(explode('-', $this->getState('id'), 2), 2, null); + + if ($account === null) + { + throw new \Exception('Account was not set'); + } + + $provider = $providerManager->getProvider($provider); + $adapter = $providerManager->getAdapter($this->getState('id')); + + $obj = new \stdClass; + $obj->id = $provider->getID() . '-' . $adapter->getAdapterName(); + $obj->provider_id = $provider->getID(); + $obj->name = $adapter->getAdapterName(); + $obj->path = $provider->getID() . '-' . $adapter->getAdapterName() . ':/'; + + return $obj; + } +} diff --git a/api/components/com_media/src/Model/AdaptersModel.php b/api/components/com_media/src/Model/AdaptersModel.php new file mode 100644 index 0000000000000..ef8e8bca6aef7 --- /dev/null +++ b/api/components/com_media/src/Model/AdaptersModel.php @@ -0,0 +1,113 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\Component\Media\Api\Model; + +\defined('_JEXEC') or die; + +use Joomla\CMS\Factory; +use Joomla\CMS\MVC\Model\BaseModel; +use Joomla\CMS\MVC\Model\ListModelInterface; +use Joomla\CMS\Pagination\Pagination; +use Joomla\CMS\Plugin\PluginHelper; +use Joomla\Component\Media\Administrator\Event\MediaProviderEvent; +use Joomla\Component\Media\Administrator\Provider\ProviderManager; + +/** + * Media web service model supporting lists of media adapters. + * + * @since __DEPLOY_VERSION__ + */ +class AdaptersModel extends BaseModel implements ListModelInterface +{ + /** + * A hacky way to enable the standard jsonapiView::displayList() to create a Pagination object, + * since com_media's ApiModel does not support pagination as we know from regular ListModel derived models. + * + * @var int + * @since __DEPLOY_VERSION__ + */ + private $total = 0; + + /** + * Method to get a list of files and/or folders. + * + * @return array An array of data items. + * + * @since __DEPLOY_VERSION__ + */ + public function getItems(): array + { + $providerManager = new ProviderManager; + + // Fire the event to get the results + $eventParameters = ['context' => 'AdapterManager', 'providerManager' => $providerManager]; + $event = new MediaProviderEvent('onSetupProviders', $eventParameters); + PluginHelper::importPlugin('filesystem'); + Factory::getApplication()->getDispatcher()->dispatch('onSetupProviders', $event); + + $adapters = []; + foreach($providerManager->getProviders() as $provider) + { + foreach ($provider->getAdapters() as $adapter) + { + $obj = new \stdClass; + $obj->id = $provider->getID() . '-' . $adapter->getAdapterName(); + $obj->provider_id = $provider->getID(); + $obj->name = $adapter->getAdapterName(); + $obj->path = $provider->getID() . '-' . $adapter->getAdapterName() . ':/'; + + $adapters[] = $obj; + } + } + + // A hacky way to enable the standard jsonapiView::displayList() to create a Pagination object. + $this->total = \count($adapters); + + return $adapters; + } + + /** + * Method to get a \JPagination object for the data set. + * + * @return Pagination A Pagination object for the data set. + * + * @since __DEPLOY_VERSION__ + */ + public function getPagination(): Pagination + { + return new Pagination($this->getTotal(), $this->getStart(), 0); + } + + /** + * Method to get the starting number of items for the data set. Because com_media's ApiModel + * does not support pagination as we know from regular ListModel derived models, + * we always start at the top. + * + * @return integer The starting number of items available in the data set. + * + * @since __DEPLOY_VERSION__ + */ + public function getStart(): int + { + return 0; + } + + /** + * Method to get the total number of items for the data set. + * + * @return integer The total number of items available in the data set. + * + * @since __DEPLOY_VERSION__ + */ + public function getTotal(): int + { + return $this->total; + } +} diff --git a/api/components/com_media/src/View/Adapters/JsonapiView.php b/api/components/com_media/src/View/Adapters/JsonapiView.php new file mode 100644 index 0000000000000..7a2d05b8b2117 --- /dev/null +++ b/api/components/com_media/src/View/Adapters/JsonapiView.php @@ -0,0 +1,49 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\Component\Media\Api\View\Adapters; + +\defined('_JEXEC') or die; + +use Joomla\CMS\MVC\View\JsonApiView as BaseApiView; +use Joomla\Component\Media\Api\Helper\AdapterTrait; + +/** + * Media web service view + * + * @since __DEPLOY_VERSION__ + */ +class JsonapiView extends BaseApiView +{ + use AdapterTrait; + + /** + * The fields to render item in the documents + * + * @var array + * @since __DEPLOY_VERSION__ + */ + protected $fieldsToRenderItem = [ + 'provider_id', + 'name', + 'path', + ]; + + /** + * The fields to render items in the documents + * + * @var array + * @since __DEPLOY_VERSION__ + */ + protected $fieldsToRenderList = [ + 'provider_id', + 'name', + 'path', + ]; +} diff --git a/plugins/webservices/media/media.php b/plugins/webservices/media/media.php index d64baa88595ad..b1ebf1605453a 100644 --- a/plugins/webservices/media/media.php +++ b/plugins/webservices/media/media.php @@ -29,7 +29,7 @@ class PlgWebservicesMedia extends CMSPlugin protected $autoloadLanguage = true; /** - * Registers com_media's API's routes in the application + * Registers com_media's API's routes in the application. * * @param ApiRouter &$router The API Routing object * @@ -39,16 +39,47 @@ class PlgWebservicesMedia extends CMSPlugin */ public function onBeforeApiRoute(&$router): void { - $this->createCRUDRoutes( + $this->createAdapterReadRoutes( $router, - 'v1/media', + 'v1/media/adapters', + 'adapters', + ['component' => 'com_media'] + ); + $this->createMediaCRUDRoutes( + $router, + 'v1/media/files', 'media', ['component' => 'com_media'] ); } /** - * Creates CRUD routes. + * Creates adapter read routes. + * + * @param ApiRouter &$router The API Routing object + * @param string $baseName The base name of the component. + * @param string $controller The name of the controller that contains CRUD functions. + * @param array $defaults An array of default values that are used when the URL is matched. + * @param bool $publicGets Allow the public to make GET requests. + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + private function createAdapterReadRoutes(&$router, $baseName, $controller, $defaults = [], $publicGets = false): void + { + $getDefaults = array_merge(['public' => $publicGets], $defaults); + + $routes = [ + new Route(['GET'], $baseName, $controller . '.displayList', [], $getDefaults), + new Route(['GET'], $baseName . '/:id', $controller . '.displayItem', [], $getDefaults), + ]; + + $router->addRoutes($routes); + } + + /** + * Creates media CRUD routes. * * @param ApiRouter &$router The API Routing object * @param string $baseName The base name of the component. @@ -60,7 +91,7 @@ public function onBeforeApiRoute(&$router): void * * @since __DEPLOY_VERSION__ */ - private function createCRUDRoutes(&$router, $baseName, $controller, $defaults = [], $publicGets = false): void + private function createMediaCRUDRoutes(&$router, $baseName, $controller, $defaults = [], $publicGets = false): void { $getDefaults = array_merge(['public' => $publicGets], $defaults); From eacc0637a492d383ad0bff79a993f523e14198e2 Mon Sep 17 00:00:00 2001 From: Allon Moritz Date: Fri, 8 Oct 2021 13:33:26 +0200 Subject: [PATCH 28/57] Add plugin to install file --- installation/sql/mysql/base.sql | 1 + installation/sql/postgresql/base.sql | 1 + 2 files changed, 2 insertions(+) diff --git a/installation/sql/mysql/base.sql b/installation/sql/mysql/base.sql index d7a5ea81915de..82fd5d03509e1 100644 --- a/installation/sql/mysql/base.sql +++ b/installation/sql/mysql/base.sql @@ -353,6 +353,7 @@ INSERT INTO `#__extensions` (`package_id`, `name`, `type`, `element`, `folder`, (0, 'plg_webservices_content', 'plugin', 'content', 'webservices', 0, 1, 1, 0, 1, '', '{}', '', 4, 0), (0, 'plg_webservices_installer', 'plugin', 'installer', 'webservices', 0, 1, 1, 0, 1, '', '{}', '', 5, 0), (0, 'plg_webservices_languages', 'plugin', 'languages', 'webservices', 0, 1, 1, 0, 1, '', '{}', '', 6, 0), +(0, 'plg_webservices_media', 'plugin', 'media', 'webservices', 0, 1, 1, 0, 1, '', '{}', '', 7, 0), (0, 'plg_webservices_menus', 'plugin', 'menus', 'webservices', 0, 1, 1, 0, 1, '', '{}', '', 7, 0), (0, 'plg_webservices_messages', 'plugin', 'messages', 'webservices', 0, 1, 1, 0, 1, '', '{}', '', 8, 0), (0, 'plg_webservices_modules', 'plugin', 'modules', 'webservices', 0, 1, 1, 0, 1, '', '{}', '', 9, 0), diff --git a/installation/sql/postgresql/base.sql b/installation/sql/postgresql/base.sql index 55d319d745aae..8786f98bd20e3 100644 --- a/installation/sql/postgresql/base.sql +++ b/installation/sql/postgresql/base.sql @@ -359,6 +359,7 @@ INSERT INTO "#__extensions" ("package_id", "name", "type", "element", "folder", (0, 'plg_webservices_content', 'plugin', 'content', 'webservices', 0, 1, 1, 0, 1, '', '{}', '', 4, 0), (0, 'plg_webservices_installer', 'plugin', 'installer', 'webservices', 0, 1, 1, 0, 1, '', '{}', '', 5, 0), (0, 'plg_webservices_languages', 'plugin', 'languages', 'webservices', 0, 1, 1, 0, 1, '', '{}', '', 6, 0), +(0, 'plg_webservices_media', 'plugin', 'media', 'webservices', 0, 1, 1, 0, 1, '', '{}', '', 7, 0), (0, 'plg_webservices_menus', 'plugin', 'menus', 'webservices', 0, 1, 1, 0, 1, '', '{}', '', 7, 0), (0, 'plg_webservices_messages', 'plugin', 'messages', 'webservices', 0, 1, 1, 0, 1, '', '{}', '', 8, 0), (0, 'plg_webservices_modules', 'plugin', 'modules', 'webservices', 0, 1, 1, 0, 1, '', '{}', '', 9, 0), From c82367b515e44ca24b7c5ee1d056957e7c4904e6 Mon Sep 17 00:00:00 2001 From: Allon Moritz Date: Fri, 8 Oct 2021 13:34:13 +0200 Subject: [PATCH 29/57] Add tests --- composer.json | 3 +- composer.lock | 634 +++++++++++++++++- configuration.php.bak | 109 +++ tests/Codeception/_support/Helper/Api.php | 26 + .../Codeception/_support/Helper/JoomlaDb.php | 17 + tests/Codeception/acceptance.suite.dist.yml | 2 +- tests/Codeception/api/BasicCest.php | 37 +- .../api/com_banners/BannerCest.php | 48 +- .../api/com_contact/ContactCest.php | 48 +- .../api/com_content/ContentCest.php | 48 +- tests/Codeception/api/com_media/MediaCest.php | 73 ++ 11 files changed, 907 insertions(+), 138 deletions(-) create mode 100644 configuration.php.bak create mode 100644 tests/Codeception/api/com_media/MediaCest.php diff --git a/composer.json b/composer.json index a1d9f53ecbbb7..01163db2a7c10 100644 --- a/composer.json +++ b/composer.json @@ -99,7 +99,8 @@ "codeception/module-db": "^1.0", "codeception/module-rest": "^1.0", "codeception/module-webdriver": "^1.0", - "codeception/module-phpbrowser": "^1.0" + "codeception/module-phpbrowser": "^1.0", + "hoa/console": "^3.17" }, "replace": { "paragonie/random_compat": "9.99.99" diff --git a/composer.lock b/composer.lock index 473ecbc3dfc52..ccd129b5b8cd8 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": "c9a7854f652d08360c08598e74e42304", + "content-hash": "b82b470f0a520b5855d905557679aa47", "packages": [ { "name": "algo26-matthias/idna-convert", @@ -6656,6 +6656,636 @@ }, "time": "2021-06-30T20:03:07+00:00" }, + { + "name": "hoa/consistency", + "version": "1.17.05.02", + "source": { + "type": "git", + "url": "https://github.com/hoaproject/Consistency.git", + "reference": "fd7d0adc82410507f332516faf655b6ed22e4c2f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/hoaproject/Consistency/zipball/fd7d0adc82410507f332516faf655b6ed22e4c2f", + "reference": "fd7d0adc82410507f332516faf655b6ed22e4c2f", + "shasum": "" + }, + "require": { + "hoa/exception": "~1.0", + "php": ">=5.5.0" + }, + "require-dev": { + "hoa/stream": "~1.0", + "hoa/test": "~2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Hoa\\Consistency\\": "." + }, + "files": [ + "Prelude.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Ivan Enderlin", + "email": "ivan.enderlin@hoa-project.net" + }, + { + "name": "Hoa community", + "homepage": "https://hoa-project.net/" + } + ], + "description": "The Hoa\\Consistency library.", + "homepage": "https://hoa-project.net/", + "keywords": [ + "autoloader", + "callable", + "consistency", + "entity", + "flex", + "keyword", + "library" + ], + "support": { + "docs": "https://central.hoa-project.net/Documentation/Library/Consistency", + "email": "support@hoa-project.net", + "forum": "https://users.hoa-project.net/", + "irc": "irc://chat.freenode.net/hoaproject", + "issues": "https://github.com/hoaproject/Consistency/issues", + "source": "https://central.hoa-project.net/Resource/Library/Consistency" + }, + "abandoned": true, + "time": "2017-05-02T12:18:12+00:00" + }, + { + "name": "hoa/console", + "version": "3.17.05.02", + "source": { + "type": "git", + "url": "https://github.com/hoaproject/Console.git", + "reference": "e231fd3ea70e6d773576ae78de0bdc1daf331a66" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/hoaproject/Console/zipball/e231fd3ea70e6d773576ae78de0bdc1daf331a66", + "reference": "e231fd3ea70e6d773576ae78de0bdc1daf331a66", + "shasum": "" + }, + "require": { + "hoa/consistency": "~1.0", + "hoa/event": "~1.0", + "hoa/exception": "~1.0", + "hoa/file": "~1.0", + "hoa/protocol": "~1.0", + "hoa/stream": "~1.0", + "hoa/ustring": "~4.0" + }, + "require-dev": { + "hoa/test": "~2.0" + }, + "suggest": { + "ext-pcntl": "To enable hoa://Event/Console/Window:resize.", + "hoa/dispatcher": "To use the console kit.", + "hoa/router": "To use the console kit." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Hoa\\Console\\": "." + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Ivan Enderlin", + "email": "ivan.enderlin@hoa-project.net" + }, + { + "name": "Hoa community", + "homepage": "https://hoa-project.net/" + } + ], + "description": "The Hoa\\Console library.", + "homepage": "https://hoa-project.net/", + "keywords": [ + "autocompletion", + "chrome", + "cli", + "console", + "cursor", + "getoption", + "library", + "option", + "parser", + "processus", + "readline", + "terminfo", + "tput", + "window" + ], + "support": { + "docs": "https://central.hoa-project.net/Documentation/Library/Console", + "email": "support@hoa-project.net", + "forum": "https://users.hoa-project.net/", + "irc": "irc://chat.freenode.net/hoaproject", + "issues": "https://github.com/hoaproject/Console/issues", + "source": "https://central.hoa-project.net/Resource/Library/Console" + }, + "abandoned": true, + "time": "2017-05-02T12:26:19+00:00" + }, + { + "name": "hoa/event", + "version": "1.17.01.13", + "source": { + "type": "git", + "url": "https://github.com/hoaproject/Event.git", + "reference": "6c0060dced212ffa3af0e34bb46624f990b29c54" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/hoaproject/Event/zipball/6c0060dced212ffa3af0e34bb46624f990b29c54", + "reference": "6c0060dced212ffa3af0e34bb46624f990b29c54", + "shasum": "" + }, + "require": { + "hoa/consistency": "~1.0", + "hoa/exception": "~1.0" + }, + "require-dev": { + "hoa/test": "~2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Hoa\\Event\\": "." + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Ivan Enderlin", + "email": "ivan.enderlin@hoa-project.net" + }, + { + "name": "Hoa community", + "homepage": "https://hoa-project.net/" + } + ], + "description": "The Hoa\\Event library.", + "homepage": "https://hoa-project.net/", + "keywords": [ + "event", + "library", + "listener", + "observer" + ], + "support": { + "docs": "https://central.hoa-project.net/Documentation/Library/Event", + "email": "support@hoa-project.net", + "forum": "https://users.hoa-project.net/", + "irc": "irc://chat.freenode.net/hoaproject", + "issues": "https://github.com/hoaproject/Event/issues", + "source": "https://central.hoa-project.net/Resource/Library/Event" + }, + "abandoned": true, + "time": "2017-01-13T15:30:50+00:00" + }, + { + "name": "hoa/exception", + "version": "1.17.01.16", + "source": { + "type": "git", + "url": "https://github.com/hoaproject/Exception.git", + "reference": "091727d46420a3d7468ef0595651488bfc3a458f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/hoaproject/Exception/zipball/091727d46420a3d7468ef0595651488bfc3a458f", + "reference": "091727d46420a3d7468ef0595651488bfc3a458f", + "shasum": "" + }, + "require": { + "hoa/consistency": "~1.0", + "hoa/event": "~1.0" + }, + "require-dev": { + "hoa/test": "~2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Hoa\\Exception\\": "." + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Ivan Enderlin", + "email": "ivan.enderlin@hoa-project.net" + }, + { + "name": "Hoa community", + "homepage": "https://hoa-project.net/" + } + ], + "description": "The Hoa\\Exception library.", + "homepage": "https://hoa-project.net/", + "keywords": [ + "exception", + "library" + ], + "support": { + "docs": "https://central.hoa-project.net/Documentation/Library/Exception", + "email": "support@hoa-project.net", + "forum": "https://users.hoa-project.net/", + "irc": "irc://chat.freenode.net/hoaproject", + "issues": "https://github.com/hoaproject/Exception/issues", + "source": "https://central.hoa-project.net/Resource/Library/Exception" + }, + "abandoned": true, + "time": "2017-01-16T07:53:27+00:00" + }, + { + "name": "hoa/file", + "version": "1.17.07.11", + "source": { + "type": "git", + "url": "https://github.com/hoaproject/File.git", + "reference": "35cb979b779bc54918d2f9a4e02ed6c7a1fa67ca" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/hoaproject/File/zipball/35cb979b779bc54918d2f9a4e02ed6c7a1fa67ca", + "reference": "35cb979b779bc54918d2f9a4e02ed6c7a1fa67ca", + "shasum": "" + }, + "require": { + "hoa/consistency": "~1.0", + "hoa/event": "~1.0", + "hoa/exception": "~1.0", + "hoa/iterator": "~2.0", + "hoa/stream": "~1.0" + }, + "require-dev": { + "hoa/test": "~2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Hoa\\File\\": "." + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Ivan Enderlin", + "email": "ivan.enderlin@hoa-project.net" + }, + { + "name": "Hoa community", + "homepage": "https://hoa-project.net/" + } + ], + "description": "The Hoa\\File library.", + "homepage": "https://hoa-project.net/", + "keywords": [ + "Socket", + "directory", + "file", + "finder", + "library", + "link", + "temporary" + ], + "support": { + "docs": "https://central.hoa-project.net/Documentation/Library/File", + "email": "support@hoa-project.net", + "forum": "https://users.hoa-project.net/", + "irc": "irc://chat.freenode.net/hoaproject", + "issues": "https://github.com/hoaproject/File/issues", + "source": "https://central.hoa-project.net/Resource/Library/File" + }, + "abandoned": true, + "time": "2017-07-11T07:42:15+00:00" + }, + { + "name": "hoa/iterator", + "version": "2.17.01.10", + "source": { + "type": "git", + "url": "https://github.com/hoaproject/Iterator.git", + "reference": "d1120ba09cb4ccd049c86d10058ab94af245f0cc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/hoaproject/Iterator/zipball/d1120ba09cb4ccd049c86d10058ab94af245f0cc", + "reference": "d1120ba09cb4ccd049c86d10058ab94af245f0cc", + "shasum": "" + }, + "require": { + "hoa/consistency": "~1.0", + "hoa/exception": "~1.0" + }, + "require-dev": { + "hoa/test": "~2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Hoa\\Iterator\\": "." + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Ivan Enderlin", + "email": "ivan.enderlin@hoa-project.net" + }, + { + "name": "Hoa community", + "homepage": "https://hoa-project.net/" + } + ], + "description": "The Hoa\\Iterator library.", + "homepage": "https://hoa-project.net/", + "keywords": [ + "iterator", + "library" + ], + "support": { + "docs": "https://central.hoa-project.net/Documentation/Library/Iterator", + "email": "support@hoa-project.net", + "forum": "https://users.hoa-project.net/", + "irc": "irc://chat.freenode.net/hoaproject", + "issues": "https://github.com/hoaproject/Iterator/issues", + "source": "https://central.hoa-project.net/Resource/Library/Iterator" + }, + "abandoned": true, + "time": "2017-01-10T10:34:47+00:00" + }, + { + "name": "hoa/protocol", + "version": "1.17.01.14", + "source": { + "type": "git", + "url": "https://github.com/hoaproject/Protocol.git", + "reference": "5c2cf972151c45f373230da170ea015deecf19e2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/hoaproject/Protocol/zipball/5c2cf972151c45f373230da170ea015deecf19e2", + "reference": "5c2cf972151c45f373230da170ea015deecf19e2", + "shasum": "" + }, + "require": { + "hoa/consistency": "~1.0", + "hoa/exception": "~1.0" + }, + "require-dev": { + "hoa/test": "~2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Hoa\\Protocol\\": "." + }, + "files": [ + "Wrapper.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Ivan Enderlin", + "email": "ivan.enderlin@hoa-project.net" + }, + { + "name": "Hoa community", + "homepage": "https://hoa-project.net/" + } + ], + "description": "The Hoa\\Protocol library.", + "homepage": "https://hoa-project.net/", + "keywords": [ + "library", + "protocol", + "resource", + "stream", + "wrapper" + ], + "support": { + "docs": "https://central.hoa-project.net/Documentation/Library/Protocol", + "email": "support@hoa-project.net", + "forum": "https://users.hoa-project.net/", + "irc": "irc://chat.freenode.net/hoaproject", + "issues": "https://github.com/hoaproject/Protocol/issues", + "source": "https://central.hoa-project.net/Resource/Library/Protocol" + }, + "abandoned": true, + "time": "2017-01-14T12:26:10+00:00" + }, + { + "name": "hoa/stream", + "version": "1.17.02.21", + "source": { + "type": "git", + "url": "https://github.com/hoaproject/Stream.git", + "reference": "3293cfffca2de10525df51436adf88a559151d82" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/hoaproject/Stream/zipball/3293cfffca2de10525df51436adf88a559151d82", + "reference": "3293cfffca2de10525df51436adf88a559151d82", + "shasum": "" + }, + "require": { + "hoa/consistency": "~1.0", + "hoa/event": "~1.0", + "hoa/exception": "~1.0", + "hoa/protocol": "~1.0" + }, + "require-dev": { + "hoa/test": "~2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Hoa\\Stream\\": "." + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Ivan Enderlin", + "email": "ivan.enderlin@hoa-project.net" + }, + { + "name": "Hoa community", + "homepage": "https://hoa-project.net/" + } + ], + "description": "The Hoa\\Stream library.", + "homepage": "https://hoa-project.net/", + "keywords": [ + "Context", + "bucket", + "composite", + "filter", + "in", + "library", + "out", + "protocol", + "stream", + "wrapper" + ], + "support": { + "docs": "https://central.hoa-project.net/Documentation/Library/Stream", + "email": "support@hoa-project.net", + "forum": "https://users.hoa-project.net/", + "irc": "irc://chat.freenode.net/hoaproject", + "issues": "https://github.com/hoaproject/Stream/issues", + "source": "https://central.hoa-project.net/Resource/Library/Stream" + }, + "abandoned": true, + "time": "2017-02-21T16:01:06+00:00" + }, + { + "name": "hoa/ustring", + "version": "4.17.01.16", + "source": { + "type": "git", + "url": "https://github.com/hoaproject/Ustring.git", + "reference": "e6326e2739178799b1fe3fdd92029f9517fa17a0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/hoaproject/Ustring/zipball/e6326e2739178799b1fe3fdd92029f9517fa17a0", + "reference": "e6326e2739178799b1fe3fdd92029f9517fa17a0", + "shasum": "" + }, + "require": { + "hoa/consistency": "~1.0", + "hoa/exception": "~1.0" + }, + "require-dev": { + "hoa/test": "~2.0" + }, + "suggest": { + "ext-iconv": "ext/iconv must be present (or a third implementation) to use Hoa\\Ustring::transcode().", + "ext-intl": "To get a better Hoa\\Ustring::toAscii() and Hoa\\Ustring::compareTo()." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.x-dev" + } + }, + "autoload": { + "psr-4": { + "Hoa\\Ustring\\": "." + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Ivan Enderlin", + "email": "ivan.enderlin@hoa-project.net" + }, + { + "name": "Hoa community", + "homepage": "https://hoa-project.net/" + } + ], + "description": "The Hoa\\Ustring library.", + "homepage": "https://hoa-project.net/", + "keywords": [ + "library", + "search", + "string", + "unicode" + ], + "support": { + "docs": "https://central.hoa-project.net/Documentation/Library/Ustring", + "email": "support@hoa-project.net", + "forum": "https://users.hoa-project.net/", + "irc": "irc://chat.freenode.net/hoaproject", + "issues": "https://github.com/hoaproject/Ustring/issues", + "source": "https://central.hoa-project.net/Resource/Library/Ustring" + }, + "abandoned": true, + "time": "2017-01-16T07:08:25+00:00" + }, { "name": "joomla-projects/joomla-browser", "version": "v4.0.0.x-dev", @@ -9739,5 +10369,5 @@ "platform-overrides": { "php": "7.2.5" }, - "plugin-api-version": "2.1.0" + "plugin-api-version": "2.0.0" } diff --git a/configuration.php.bak b/configuration.php.bak new file mode 100644 index 0000000000000..a3153bc3b7d16 --- /dev/null +++ b/configuration.php.bak @@ -0,0 +1,109 @@ +Please check back again soon.'; + public $display_offline_message = '1'; + public $offline_image = ''; + public $sitename = 'Joomla cms4'; + public $editor = 'tinymce'; + public $captcha = '0'; + public $list_limit = '20'; + public $access = '1'; + public $debug = '1'; + public $debug_lang = '0'; + public $debug_lang_const = '1'; + public $dbtype = 'mysqli'; + public $host = 'mysql'; + public $user = 'root'; + public $password = 'root'; + public $db = 'joomla_cms4'; + public $dbprefix = 'j_'; + public $dbencryption = '0'; + public $dbsslverifyservercert = ''; + public $dbsslkey = ''; + public $dbsslcert = ''; + public $dbsslca = ''; + public $dbsslcipher = ''; + public $live_site = ''; + public $secret = 'XgrJSL137VSjPBVn'; + public $gzip = '0'; + public $error_reporting = 'development'; + public $helpurl = 'https://help.joomla.org/proxy?keyref=Help{major}{minor}:{keyref}&lang={langcode}'; + public $ftp_host = ''; + public $ftp_port = ''; + public $ftp_user = ''; + public $ftp_pass = ''; + public $ftp_root = ''; + public $ftp_enable = '0'; + public $offset = 'UTC'; + public $mailonline = '1'; + public $mailer = 'smtp'; + public $mailfrom = 'admin@example.com'; + public $fromname = 'Joomla cms4'; + public $sendmail = '/usr/sbin/sendmail'; + public $smtpauth = '0'; + public $smtpuser = ''; + public $smtppass = ''; + public $smtphost = 'mailcatcher'; + public $smtpsecure = 'none'; + public $smtpport = '1025'; + public $caching = '0'; + public $cache_handler = 'file'; + public $cachetime = '15'; + public $cache_platformprefix = '0'; + public $MetaDesc = ''; + public $MetaTitle = '1'; + public $MetaAuthor = '1'; + public $MetaVersion = '0'; + public $robots = ''; + public $sef = '1'; + public $sef_rewrite = '1'; + public $sef_suffix = '0'; + public $unicodeslugs = '0'; + public $feed_limit = '10'; + public $feed_email = 'none'; + public $log_path = '/var/www/html/cms4/administrator/logs'; + public $tmp_path = '/var/www/html/cms4/tmp'; + public $lifetime = '9999'; + public $session_handler = 'database'; + public $shared_session = '0'; + public $session_metadata = '1'; + public $memcache_persist = '1'; + public $memcache_compress = '0'; + public $memcache_server_host = 'localhost'; + public $memcache_server_port = '11211'; + public $memcached_persist = '1'; + public $memcached_compress = '0'; + public $memcached_server_host = 'localhost'; + public $memcached_server_port = '11211'; + public $redis_persist = '1'; + public $redis_server_host = 'localhost'; + public $redis_server_port = '6379'; + public $redis_server_auth = ''; + public $redis_server_db = '0'; + public $proxy_enable = '0'; + public $proxy_host = ''; + public $proxy_port = ''; + public $proxy_user = ''; + public $proxy_pass = ''; + public $massmailoff = '0'; + public $replyto = ''; + public $replytoname = ''; + public $MetaKeys = ''; + public $MetaRights = ''; + public $sitename_pagetitles = '0'; + public $force_ssl = '0'; + public $session_memcache_server_host = 'localhost'; + public $session_memcache_server_port = '11211'; + public $session_memcached_server_host = 'localhost'; + public $session_memcached_server_port = '11211'; + public $session_redis_persist = '1'; + public $session_redis_server_host = 'localhost'; + public $session_redis_server_port = '6379'; + public $session_redis_server_auth = ''; + public $session_redis_server_db = '0'; + public $frontediting = '1'; + public $cookie_domain = ''; + public $cookie_path = ''; + public $asset_id = '1'; +} diff --git a/tests/Codeception/_support/Helper/Api.php b/tests/Codeception/_support/Helper/Api.php index c0c4e97747a7e..c1cfa3a17cdb0 100644 --- a/tests/Codeception/_support/Helper/Api.php +++ b/tests/Codeception/_support/Helper/Api.php @@ -21,4 +21,30 @@ */ class Api extends Module { + /** + * Creates a user for API authentication and returns a bearer token. + * + * @return string The token + * + * @since __DEPLOY_VERSION__ + */ + public function getBearerToken(): string + { + /** @var JoomlaDb $db */ + $db = $this->getModule('Helper\\JoomlaDb'); + + $desiredUserId = 3; + + if (!$db->grabFromDatabase('users', 'id', ['id' => $desiredUserId])) + { + $db->haveInDatabase('users', ['id' => $desiredUserId, 'name' => 'API', 'email' => 'api@example.com', 'username' => 'api', 'password' => '123', 'block' => 0], []); + $db->haveInDatabase('user_usergroup_map', ['user_id' => $desiredUserId, 'group_id' => 8]); + $enabledData = ['user_id' => $desiredUserId, 'profile_key' => 'joomlatoken.enabled', 'profile_value' => 1]; + $tokenData = ['user_id' => $desiredUserId, 'profile_key' => 'joomlatoken.token', 'profile_value' => 'dOi2m1NRrnBHlhaWK/WWxh3B5tqq1INbdf4DhUmYTI4=']; + $db->haveInDatabase('user_profiles', $enabledData); + $db->haveInDatabase('user_profiles', $tokenData); + } + + return 'c2hhMjU2OjM6ZTJmMjJlYTNlNTU0NmM1MDJhYTIzYzMwN2MxYzAwZTQ5NzJhMWRmOTUyNjY5MTk2YjE5ODJmZWMwZTcxNzgwMQ=='; + } } diff --git a/tests/Codeception/_support/Helper/JoomlaDb.php b/tests/Codeception/_support/Helper/JoomlaDb.php index d508c65704a63..d3a892b6eda73 100644 --- a/tests/Codeception/_support/Helper/JoomlaDb.php +++ b/tests/Codeception/_support/Helper/JoomlaDb.php @@ -164,6 +164,23 @@ public function updateInDatabase($table, array $data, array $criteria = []) parent::updateInDatabase($table, $data, $criteria); } + /** + * Deletes records in a database. + * + * @param string $table Table name + * @param array $criteria Search criteria [Optional] + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public function deleteFromDatabase($table, $criteria = []): void + { + $table = $this->addPrefix($table); + + $this->driver->deleteQueryByCriteria($table, $criteria); + } + /** * Add the table prefix. * diff --git a/tests/Codeception/acceptance.suite.dist.yml b/tests/Codeception/acceptance.suite.dist.yml index 8222ec9091571..b7d67a979f639 100644 --- a/tests/Codeception/acceptance.suite.dist.yml +++ b/tests/Codeception/acceptance.suite.dist.yml @@ -17,7 +17,7 @@ modules: window_size: 1920x1080 capabilities: 'goog:chromeOptions': - args: ["headless", "whitelisted-ips", "disable-gpu", "no-sandbox", "window-size=1920x1080", "--disable-dev-shm-usage"] + args: ["whitelisted-ips", "disable-gpu", "no-sandbox", "window-size=1920x1080", "--disable-dev-shm-usage"] name: 'jane doe' # Name for the Administrator username: 'ci-admin' # UserName for the Administrator password: 'joomla-17082005' # Password for the Administrator diff --git a/tests/Codeception/api/BasicCest.php b/tests/Codeception/api/BasicCest.php index 784628f2848ea..8e30340b758c1 100644 --- a/tests/Codeception/api/BasicCest.php +++ b/tests/Codeception/api/BasicCest.php @@ -17,39 +17,6 @@ */ class BasicCest { - /** - * Api test before running. - * - * @param mixed ApiTester $I Api tester - * - * @return void - * - * @since 4.0.0 - */ - public function _before(ApiTester $I) - { - // TODO: Improve this to retrieve a specific ID to replace with a known ID - $desiredUserId = 3; - $I->updateInDatabase('users', ['id' => 3], []); - $I->updateInDatabase('user_usergroup_map', ['user_id' => 3], []); - $enabledData = ['user_id' => $desiredUserId, 'profile_key' => 'joomlatoken.enabled', 'profile_value' => 1]; - $tokenData = ['user_id' => $desiredUserId, 'profile_key' => 'joomlatoken.token', 'profile_value' => 'dOi2m1NRrnBHlhaWK/WWxh3B5tqq1INbdf4DhUmYTI4=']; - $I->haveInDatabase('user_profiles', $enabledData); - $I->haveInDatabase('user_profiles', $tokenData); - } - - /** - * Api test after running. - * - * @param mixed ApiTester $I Api tester - * - * @return void - * @since 4.0.0 - */ - public function _after(ApiTester $I) - { - } - /** * Test logging in with wrong credentials. * @@ -78,7 +45,7 @@ public function testWrongCredentials(ApiTester $I) */ public function testContentNegotiation(ApiTester $I) { - $I->amBearerAuthenticated('c2hhMjU2OjM6ZTJmMjJlYTNlNTU0NmM1MDJhYTIzYzMwN2MxYzAwZTQ5NzJhMWRmOTUyNjY5MTk2YjE5ODJmZWMwZTcxNzgwMQ=='); + $I->amBearerAuthenticated($I->getBearerToken()); $I->haveHttpHeader('Accept', 'text/xml'); $I->sendGET('/content/articles/1'); $I->seeResponseCodeIs(Codeception\Util\HttpCode::NOT_ACCEPTABLE); @@ -95,7 +62,7 @@ public function testContentNegotiation(ApiTester $I) */ public function testRouteNotFound(ApiTester $I) { - $I->amBearerAuthenticated('c2hhMjU2OjM6ZTJmMjJlYTNlNTU0NmM1MDJhYTIzYzMwN2MxYzAwZTQ5NzJhMWRmOTUyNjY5MTk2YjE5ODJmZWMwZTcxNzgwMQ=='); + $I->amBearerAuthenticated($I->getBearerToken()); $I->haveHttpHeader('Accept', 'application/vnd.api+json'); $I->sendGET('/not/existing/1'); $I->seeResponseCodeIs(Codeception\Util\HttpCode::NOT_FOUND); diff --git a/tests/Codeception/api/com_banners/BannerCest.php b/tests/Codeception/api/com_banners/BannerCest.php index d2ac914ab6d0e..cf03b48f5faf6 100644 --- a/tests/Codeception/api/com_banners/BannerCest.php +++ b/tests/Codeception/api/com_banners/BannerCest.php @@ -25,31 +25,12 @@ class BannerCest * * @return void * - * @since 4.0.0 + * @since __DEPLOY_VERSION__ */ public function _before(ApiTester $I) { - // TODO: Improve this to retrieve a specific ID to replace with a known ID - $desiredUserId = 3; - $I->updateInDatabase('users', ['id' => 3], []); - $I->updateInDatabase('user_usergroup_map', ['user_id' => 3], []); - $enabledData = ['user_id' => $desiredUserId, 'profile_key' => 'joomlatoken.enabled', 'profile_value' => 1]; - $tokenData = ['user_id' => $desiredUserId, 'profile_key' => 'joomlatoken.token', 'profile_value' => 'dOi2m1NRrnBHlhaWK/WWxh3B5tqq1INbdf4DhUmYTI4=']; - $I->haveInDatabase('user_profiles', $enabledData); - $I->haveInDatabase('user_profiles', $tokenData); - } - - /** - * Api test after running. - * - * @param mixed ApiTester $I Api tester - * - * @return void - * - * @since 4.0.0 - */ - public function _after(ApiTester $I) - { + $I->deleteFromDatabase('banners'); + $I->deleteFromDatabase('categories', ['id >' => 7]); } /** @@ -65,7 +46,7 @@ public function _after(ApiTester $I) */ public function testCrudOnBanner(ApiTester $I) { - $I->amBearerAuthenticated('c2hhMjU2OjM6ZTJmMjJlYTNlNTU0NmM1MDJhYTIzYzMwN2MxYzAwZTQ5NzJhMWRmOTUyNjY5MTk2YjE5ODJmZWMwZTcxNzgwMQ=='); + $I->amBearerAuthenticated($I->getBearerToken()); $I->haveHttpHeader('Content-Type', 'application/json'); $I->haveHttpHeader('Accept', 'application/vnd.api+json'); @@ -86,23 +67,24 @@ public function testCrudOnBanner(ApiTester $I) $I->sendPOST('/banners', $testBanner); $I->seeResponseCodeIs(HttpCode::OK); + $id = $I->grabDataFromResponseByJsonPath('$.data.id')[0]; - $I->amBearerAuthenticated('c2hhMjU2OjM6ZTJmMjJlYTNlNTU0NmM1MDJhYTIzYzMwN2MxYzAwZTQ5NzJhMWRmOTUyNjY5MTk2YjE5ODJmZWMwZTcxNzgwMQ=='); + $I->amBearerAuthenticated($I->getBearerToken()); $I->haveHttpHeader('Accept', 'application/vnd.api+json'); - $I->sendGET('/banners/1'); + $I->sendGET('/banners/' . $id); $I->seeResponseCodeIs(HttpCode::OK); - $I->amBearerAuthenticated('c2hhMjU2OjM6ZTJmMjJlYTNlNTU0NmM1MDJhYTIzYzMwN2MxYzAwZTQ5NzJhMWRmOTUyNjY5MTk2YjE5ODJmZWMwZTcxNzgwMQ=='); + $I->amBearerAuthenticated($I->getBearerToken()); $I->haveHttpHeader('Content-Type', 'application/json'); $I->haveHttpHeader('Accept', 'application/vnd.api+json'); // Category is a required field for this patch request for now TODO: Remove this dependency - $I->sendPATCH('/banners/1', ['name' => 'Different Custom Advert', 'state' => -2, 'catid' => 3]); + $I->sendPATCH('/banners/' . $id, ['name' => 'Different Custom Advert', 'state' => -2, 'catid' => 3]); $I->seeResponseCodeIs(HttpCode::OK); - $I->amBearerAuthenticated('c2hhMjU2OjM6ZTJmMjJlYTNlNTU0NmM1MDJhYTIzYzMwN2MxYzAwZTQ5NzJhMWRmOTUyNjY5MTk2YjE5ODJmZWMwZTcxNzgwMQ=='); + $I->amBearerAuthenticated($I->getBearerToken()); $I->haveHttpHeader('Accept', 'application/vnd.api+json'); - $I->sendDELETE('/banners/1'); + $I->sendDELETE('/banners/' . $id); $I->seeResponseCodeIs(HttpCode::NO_CONTENT); } @@ -119,7 +101,7 @@ public function testCrudOnBanner(ApiTester $I) */ public function testCrudOnCategory(ApiTester $I) { - $I->amBearerAuthenticated('c2hhMjU2OjM6ZTJmMjJlYTNlNTU0NmM1MDJhYTIzYzMwN2MxYzAwZTQ5NzJhMWRmOTUyNjY5MTk2YjE5ODJmZWMwZTcxNzgwMQ=='); + $I->amBearerAuthenticated($I->getBearerToken()); $I->haveHttpHeader('Content-Type', 'application/json'); $I->haveHttpHeader('Accept', 'application/vnd.api+json'); @@ -133,12 +115,12 @@ public function testCrudOnCategory(ApiTester $I) $I->seeResponseCodeIs(HttpCode::OK); $categoryId = $I->grabDataFromResponseByJsonPath('$.data.id')[0]; - $I->amBearerAuthenticated('c2hhMjU2OjM6ZTJmMjJlYTNlNTU0NmM1MDJhYTIzYzMwN2MxYzAwZTQ5NzJhMWRmOTUyNjY5MTk2YjE5ODJmZWMwZTcxNzgwMQ=='); + $I->amBearerAuthenticated($I->getBearerToken()); $I->haveHttpHeader('Accept', 'application/vnd.api+json'); $I->sendGET('/banners/categories/' . $categoryId); $I->seeResponseCodeIs(HttpCode::OK); - $I->amBearerAuthenticated('c2hhMjU2OjM6ZTJmMjJlYTNlNTU0NmM1MDJhYTIzYzMwN2MxYzAwZTQ5NzJhMWRmOTUyNjY5MTk2YjE5ODJmZWMwZTcxNzgwMQ=='); + $I->amBearerAuthenticated($I->getBearerToken()); $I->haveHttpHeader('Content-Type', 'application/json'); $I->haveHttpHeader('Accept', 'application/vnd.api+json'); @@ -146,7 +128,7 @@ public function testCrudOnCategory(ApiTester $I) $I->sendPATCH('/banners/categories/' . $categoryId, ['title' => 'Another Title', 'published' => -2]); $I->seeResponseCodeIs(HttpCode::OK); - $I->amBearerAuthenticated('c2hhMjU2OjM6ZTJmMjJlYTNlNTU0NmM1MDJhYTIzYzMwN2MxYzAwZTQ5NzJhMWRmOTUyNjY5MTk2YjE5ODJmZWMwZTcxNzgwMQ=='); + $I->amBearerAuthenticated($I->getBearerToken()); $I->haveHttpHeader('Accept', 'application/vnd.api+json'); $I->sendDELETE('/banners/categories/' . $categoryId); $I->seeResponseCodeIs(HttpCode::NO_CONTENT); diff --git a/tests/Codeception/api/com_contact/ContactCest.php b/tests/Codeception/api/com_contact/ContactCest.php index d2bad11bb63d7..b3c179db0b5ee 100644 --- a/tests/Codeception/api/com_contact/ContactCest.php +++ b/tests/Codeception/api/com_contact/ContactCest.php @@ -25,31 +25,12 @@ class ContactCest * * @return void * - * @since 4.0.0 + * @since __DEPLOY_VERSION__ */ public function _before(ApiTester $I) { - // TODO: Improve this to retrieve a specific ID to replace with a known ID - $desiredUserId = 3; - $I->updateInDatabase('users', ['id' => 3], []); - $I->updateInDatabase('user_usergroup_map', ['user_id' => 3], []); - $enabledData = ['user_id' => $desiredUserId, 'profile_key' => 'joomlatoken.enabled', 'profile_value' => 1]; - $tokenData = ['user_id' => $desiredUserId, 'profile_key' => 'joomlatoken.token', 'profile_value' => 'dOi2m1NRrnBHlhaWK/WWxh3B5tqq1INbdf4DhUmYTI4=']; - $I->haveInDatabase('user_profiles', $enabledData); - $I->haveInDatabase('user_profiles', $tokenData); - } - - /** - * Api test after running. - * - * @param mixed ApiTester $I Api tester - * - * @return void - * - * @since 4.0.0 - */ - public function _after(ApiTester $I) - { + $I->deleteFromDatabase('contact_details'); + $I->deleteFromDatabase('categories', ['id >' => 7]); } /** @@ -65,7 +46,7 @@ public function _after(ApiTester $I) */ public function testCrudOnContact(ApiTester $I) { - $I->amBearerAuthenticated('c2hhMjU2OjM6ZTJmMjJlYTNlNTU0NmM1MDJhYTIzYzMwN2MxYzAwZTQ5NzJhMWRmOTUyNjY5MTk2YjE5ODJmZWMwZTcxNzgwMQ=='); + $I->amBearerAuthenticated($I->getBearerToken()); $I->haveHttpHeader('Content-Type', 'application/json'); $I->haveHttpHeader('Accept', 'application/vnd.api+json'); @@ -79,23 +60,24 @@ public function testCrudOnContact(ApiTester $I) $I->sendPOST('/contacts', $testarticle); $I->seeResponseCodeIs(HttpCode::OK); + $id = $I->grabDataFromResponseByJsonPath('$.data.id')[0]; - $I->amBearerAuthenticated('c2hhMjU2OjM6ZTJmMjJlYTNlNTU0NmM1MDJhYTIzYzMwN2MxYzAwZTQ5NzJhMWRmOTUyNjY5MTk2YjE5ODJmZWMwZTcxNzgwMQ=='); + $I->amBearerAuthenticated($I->getBearerToken()); $I->haveHttpHeader('Accept', 'application/vnd.api+json'); - $I->sendGET('/contacts/1'); + $I->sendGET('/contacts/' . $id); $I->seeResponseCodeIs(HttpCode::OK); - $I->amBearerAuthenticated('c2hhMjU2OjM6ZTJmMjJlYTNlNTU0NmM1MDJhYTIzYzMwN2MxYzAwZTQ5NzJhMWRmOTUyNjY5MTk2YjE5ODJmZWMwZTcxNzgwMQ=='); + $I->amBearerAuthenticated($I->getBearerToken()); $I->haveHttpHeader('Content-Type', 'application/json'); $I->haveHttpHeader('Accept', 'application/vnd.api+json'); // Category is a required field for this patch request for now TODO: Remove this dependency - $I->sendPATCH('/contacts/1', ['name' => 'Frankie Blogs', 'catid' => 4, 'published' => -2]); + $I->sendPATCH('/contacts/' . $id, ['name' => 'Frankie Blogs', 'catid' => 4, 'published' => -2]); $I->seeResponseCodeIs(HttpCode::OK); - $I->amBearerAuthenticated('c2hhMjU2OjM6ZTJmMjJlYTNlNTU0NmM1MDJhYTIzYzMwN2MxYzAwZTQ5NzJhMWRmOTUyNjY5MTk2YjE5ODJmZWMwZTcxNzgwMQ=='); + $I->amBearerAuthenticated($I->getBearerToken()); $I->haveHttpHeader('Accept', 'application/vnd.api+json'); - $I->sendDELETE('/contacts/1'); + $I->sendDELETE('/contacts/' . $id); $I->seeResponseCodeIs(HttpCode::NO_CONTENT); } @@ -112,7 +94,7 @@ public function testCrudOnContact(ApiTester $I) */ public function testCrudOnCategory(ApiTester $I) { - $I->amBearerAuthenticated('c2hhMjU2OjM6ZTJmMjJlYTNlNTU0NmM1MDJhYTIzYzMwN2MxYzAwZTQ5NzJhMWRmOTUyNjY5MTk2YjE5ODJmZWMwZTcxNzgwMQ=='); + $I->amBearerAuthenticated($I->getBearerToken()); $I->haveHttpHeader('Content-Type', 'application/json'); $I->haveHttpHeader('Accept', 'application/vnd.api+json'); @@ -129,18 +111,18 @@ public function testCrudOnCategory(ApiTester $I) $I->seeResponseCodeIs(HttpCode::OK); $categoryId = $I->grabDataFromResponseByJsonPath('$.data.id')[0]; - $I->amBearerAuthenticated('c2hhMjU2OjM6ZTJmMjJlYTNlNTU0NmM1MDJhYTIzYzMwN2MxYzAwZTQ5NzJhMWRmOTUyNjY5MTk2YjE5ODJmZWMwZTcxNzgwMQ=='); + $I->amBearerAuthenticated($I->getBearerToken()); $I->haveHttpHeader('Accept', 'application/vnd.api+json'); $I->sendGET('/contacts/categories/' . $categoryId); $I->seeResponseCodeIs(HttpCode::OK); - $I->amBearerAuthenticated('c2hhMjU2OjM6ZTJmMjJlYTNlNTU0NmM1MDJhYTIzYzMwN2MxYzAwZTQ5NzJhMWRmOTUyNjY5MTk2YjE5ODJmZWMwZTcxNzgwMQ=='); + $I->amBearerAuthenticated($I->getBearerToken()); $I->haveHttpHeader('Content-Type', 'application/json'); $I->haveHttpHeader('Accept', 'application/vnd.api+json'); $I->sendPATCH('/contacts/categories/' . $categoryId, ['title' => 'Another Title', 'published' => -2]); $I->seeResponseCodeIs(HttpCode::OK); - $I->amBearerAuthenticated('c2hhMjU2OjM6ZTJmMjJlYTNlNTU0NmM1MDJhYTIzYzMwN2MxYzAwZTQ5NzJhMWRmOTUyNjY5MTk2YjE5ODJmZWMwZTcxNzgwMQ=='); + $I->amBearerAuthenticated($I->getBearerToken()); $I->haveHttpHeader('Accept', 'application/vnd.api+json'); $I->sendDELETE('/contacts/categories/' . $categoryId); $I->seeResponseCodeIs(HttpCode::NO_CONTENT); diff --git a/tests/Codeception/api/com_content/ContentCest.php b/tests/Codeception/api/com_content/ContentCest.php index 08ac412e7785a..0896e0dfbc601 100644 --- a/tests/Codeception/api/com_content/ContentCest.php +++ b/tests/Codeception/api/com_content/ContentCest.php @@ -25,31 +25,12 @@ class ContentCest * * @return void * - * @since 4.0.0 + * @since __DEPLOY_VERSION__ */ public function _before(ApiTester $I) { - // TODO: Improve this to retrieve a specific ID to replace with a known ID - $desiredUserId = 3; - $I->updateInDatabase('users', ['id' => 3], []); - $I->updateInDatabase('user_usergroup_map', ['user_id' => 3], []); - $enabledData = ['user_id' => $desiredUserId, 'profile_key' => 'joomlatoken.enabled', 'profile_value' => 1]; - $tokenData = ['user_id' => $desiredUserId, 'profile_key' => 'joomlatoken.token', 'profile_value' => 'dOi2m1NRrnBHlhaWK/WWxh3B5tqq1INbdf4DhUmYTI4=']; - $I->haveInDatabase('user_profiles', $enabledData); - $I->haveInDatabase('user_profiles', $tokenData); - } - - /** - * Api test after running. - * - * @param mixed ApiTester $I Api tester - * - * @return void - * - * @since 4.0.0 - */ - public function _after(ApiTester $I) - { + $I->deleteFromDatabase('content'); + $I->deleteFromDatabase('categories', ['id >' => 7]); } /** @@ -65,7 +46,7 @@ public function _after(ApiTester $I) */ public function testCrudOnArticle(ApiTester $I) { - $I->amBearerAuthenticated('c2hhMjU2OjM6ZTJmMjJlYTNlNTU0NmM1MDJhYTIzYzMwN2MxYzAwZTQ5NzJhMWRmOTUyNjY5MTk2YjE5ODJmZWMwZTcxNzgwMQ=='); + $I->amBearerAuthenticated($I->getBearerToken()); $I->haveHttpHeader('Content-Type', 'application/json'); $I->haveHttpHeader('Accept', 'application/vnd.api+json'); @@ -80,21 +61,22 @@ public function testCrudOnArticle(ApiTester $I) $I->sendPOST('/content/articles', $testarticle); $I->seeResponseCodeIs(HttpCode::OK); + $id = $I->grabDataFromResponseByJsonPath('$.data.id')[0]; - $I->amBearerAuthenticated('c2hhMjU2OjM6ZTJmMjJlYTNlNTU0NmM1MDJhYTIzYzMwN2MxYzAwZTQ5NzJhMWRmOTUyNjY5MTk2YjE5ODJmZWMwZTcxNzgwMQ=='); + $I->amBearerAuthenticated($I->getBearerToken()); $I->haveHttpHeader('Accept', 'application/vnd.api+json'); - $I->sendGET('/content/articles/1'); + $I->sendGET('/content/articles/' . $id); $I->seeResponseCodeIs(HttpCode::OK); - $I->amBearerAuthenticated('c2hhMjU2OjM6ZTJmMjJlYTNlNTU0NmM1MDJhYTIzYzMwN2MxYzAwZTQ5NzJhMWRmOTUyNjY5MTk2YjE5ODJmZWMwZTcxNzgwMQ=='); + $I->amBearerAuthenticated($I->getBearerToken()); $I->haveHttpHeader('Content-Type', 'application/json'); $I->haveHttpHeader('Accept', 'application/vnd.api+json'); - $I->sendPATCH('/content/articles/1', ['title' => 'Another Title', 'state' => -2, 'catid' => 2]); + $I->sendPATCH('/content/articles/' . $id, ['title' => 'Another Title', 'state' => -2, 'catid' => 2]); $I->seeResponseCodeIs(HttpCode::OK); - $I->amBearerAuthenticated('c2hhMjU2OjM6ZTJmMjJlYTNlNTU0NmM1MDJhYTIzYzMwN2MxYzAwZTQ5NzJhMWRmOTUyNjY5MTk2YjE5ODJmZWMwZTcxNzgwMQ=='); + $I->amBearerAuthenticated($I->getBearerToken()); $I->haveHttpHeader('Accept', 'application/vnd.api+json'); - $I->sendDELETE('/content/articles/1'); + $I->sendDELETE('/content/articles/' . $id); $I->seeResponseCodeIs(HttpCode::NO_CONTENT); } @@ -112,7 +94,7 @@ public function testCrudOnArticle(ApiTester $I) public function testCrudOnCategory(ApiTester $I) { - $I->amBearerAuthenticated('c2hhMjU2OjM6ZTJmMjJlYTNlNTU0NmM1MDJhYTIzYzMwN2MxYzAwZTQ5NzJhMWRmOTUyNjY5MTk2YjE5ODJmZWMwZTcxNzgwMQ=='); + $I->amBearerAuthenticated($I->getBearerToken()); $I->haveHttpHeader('Content-Type', 'application/json'); $I->haveHttpHeader('Accept', 'application/vnd.api+json'); @@ -129,18 +111,18 @@ public function testCrudOnCategory(ApiTester $I) $I->seeResponseCodeIs(HttpCode::OK); $categoryId = $I->grabDataFromResponseByJsonPath('$.data.id')[0]; - $I->amBearerAuthenticated('c2hhMjU2OjM6ZTJmMjJlYTNlNTU0NmM1MDJhYTIzYzMwN2MxYzAwZTQ5NzJhMWRmOTUyNjY5MTk2YjE5ODJmZWMwZTcxNzgwMQ=='); + $I->amBearerAuthenticated($I->getBearerToken()); $I->haveHttpHeader('Accept', 'application/vnd.api+json'); $I->sendGET('/content/categories/' . $categoryId); $I->seeResponseCodeIs(HttpCode::OK); - $I->amBearerAuthenticated('c2hhMjU2OjM6ZTJmMjJlYTNlNTU0NmM1MDJhYTIzYzMwN2MxYzAwZTQ5NzJhMWRmOTUyNjY5MTk2YjE5ODJmZWMwZTcxNzgwMQ=='); + $I->amBearerAuthenticated($I->getBearerToken()); $I->haveHttpHeader('Content-Type', 'application/json'); $I->haveHttpHeader('Accept', 'application/vnd.api+json'); $I->sendPATCH('/content/categories/' . $categoryId, ['title' => 'Another Title', 'params' => ['workflow_id' => 'inherit'], 'published' => -2]); $I->seeResponseCodeIs(HttpCode::OK); - $I->amBearerAuthenticated('c2hhMjU2OjM6ZTJmMjJlYTNlNTU0NmM1MDJhYTIzYzMwN2MxYzAwZTQ5NzJhMWRmOTUyNjY5MTk2YjE5ODJmZWMwZTcxNzgwMQ=='); + $I->amBearerAuthenticated($I->getBearerToken()); $I->haveHttpHeader('Accept', 'application/vnd.api+json'); $I->sendDELETE('/content/categories/' . $categoryId); $I->seeResponseCodeIs(HttpCode::NO_CONTENT); diff --git a/tests/Codeception/api/com_media/MediaCest.php b/tests/Codeception/api/com_media/MediaCest.php new file mode 100644 index 0000000000000..8bf2b1fcb6671 --- /dev/null +++ b/tests/Codeception/api/com_media/MediaCest.php @@ -0,0 +1,73 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +use Codeception\Util\HttpCode; + +/** + * Class MediaCest. + * + * Basic com_media (files) tests. + * + * @since __DEPLOY_VERSION__ + */ +class MediaCest +{ + /** + * Test the media adapter endpoint of com_media from the API. + * + * @param mixed ApiTester $I Api tester + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public function testGetAdapters(ApiTester $I) + { + $I->amBearerAuthenticated($I->getBearerToken()); + $I->haveHttpHeader('Accept', 'application/vnd.api+json'); + $I->sendGET('/media/adapters'); + $I->seeResponseCodeIs(HttpCode::OK); + $I->seeResponseContainsJson(['provider_id' => 'local', 'name' => 'images']); + } + + /** + * Test the media adapter endpoint of com_media from the API. + * + * @param mixed ApiTester $I Api tester + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public function testGetAdapter(ApiTester $I) + { + $I->amBearerAuthenticated($I->getBearerToken()); + $I->haveHttpHeader('Accept', 'application/vnd.api+json'); + $I->sendGET('/media/adapters/local-images'); + $I->seeResponseCodeIs(HttpCode::OK); + $I->seeResponseContainsJson(['provider_id' => 'local', 'name' => 'images']); + } + + /** + * Test the media files endpoint of com_media from the API. + * + * @param mixed ApiTester $I Api tester + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public function testGetFiles(ApiTester $I) + { + $I->amBearerAuthenticated($I->getBearerToken()); + $I->haveHttpHeader('Accept', 'application/vnd.api+json'); + $I->sendGET('/media/files'); + $I->seeResponseCodeIs(HttpCode::OK); + } +} From 82528eb8930bc34007ec8539307a44a65ebab232 Mon Sep 17 00:00:00 2001 From: Allon Moritz Date: Thu, 21 Oct 2021 14:33:33 +0200 Subject: [PATCH 30/57] Update api/components/com_media/src/Helper/MediaHelper.php Co-authored-by: Phil E. Taylor --- api/components/com_media/src/Helper/MediaHelper.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/components/com_media/src/Helper/MediaHelper.php b/api/components/com_media/src/Helper/MediaHelper.php index dbd852e8ea73b..914460268b0fc 100644 --- a/api/components/com_media/src/Helper/MediaHelper.php +++ b/api/components/com_media/src/Helper/MediaHelper.php @@ -35,7 +35,7 @@ public static function adapterNameAndPath(String $path) $parts = explode(':', $path, 2); // If we have 2 parts, we have both an adapter name and a file path. - if (count($parts) == 2) + if (\count($parts) == 2) { $result['adapter'] = $parts[0]; $result['path'] = $parts[1]; From eb196c9dc40c9eaf561319743b12126c21264b15 Mon Sep 17 00:00:00 2001 From: Allon Moritz Date: Thu, 21 Oct 2021 14:33:43 +0200 Subject: [PATCH 31/57] Update api/components/com_media/src/Helper/MediaHelper.php Co-authored-by: Phil E. Taylor --- api/components/com_media/src/Helper/MediaHelper.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/components/com_media/src/Helper/MediaHelper.php b/api/components/com_media/src/Helper/MediaHelper.php index 914460268b0fc..4cfc7c16c8da2 100644 --- a/api/components/com_media/src/Helper/MediaHelper.php +++ b/api/components/com_media/src/Helper/MediaHelper.php @@ -47,7 +47,7 @@ public static function adapterNameAndPath(String $path) $result['adapter'] = self::defaultAdapterName(); // If we have 1 part, we return it as the path. Otherwise we return a default path. - $result['path'] = count($parts) ? $parts[0] : '/'; + $result['path'] = \count($parts) ? $parts[0] : '/'; return $result; } From 34dd7654a51d0f54740991732164ade1ad15f69e Mon Sep 17 00:00:00 2001 From: Allon Moritz Date: Thu, 21 Oct 2021 14:33:54 +0200 Subject: [PATCH 32/57] Update api/components/com_media/src/Model/MediaModel.php Co-authored-by: Phil E. Taylor --- api/components/com_media/src/Model/MediaModel.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/components/com_media/src/Model/MediaModel.php b/api/components/com_media/src/Model/MediaModel.php index ce94e0162e081..8d2a0f35d348b 100644 --- a/api/components/com_media/src/Model/MediaModel.php +++ b/api/components/com_media/src/Model/MediaModel.php @@ -87,7 +87,7 @@ public function getItems(): array * Because com_media's ApiModel does not support pagination as we know from regular ListModel * derived models, we always return all retrieved items. */ - $this->total = count($files); + $this->total = \count($files); return $files; } From 588a468fa48402ad1e0ebc7cb8037a42561c21a1 Mon Sep 17 00:00:00 2001 From: Allon Moritz Date: Thu, 21 Oct 2021 14:35:16 +0200 Subject: [PATCH 33/57] Update api/components/com_media/src/Model/MediumModel.php Co-authored-by: Phil E. Taylor --- api/components/com_media/src/Model/MediumModel.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/components/com_media/src/Model/MediumModel.php b/api/components/com_media/src/Model/MediumModel.php index 0681f6ecc898e..726690d759562 100644 --- a/api/components/com_media/src/Model/MediumModel.php +++ b/api/components/com_media/src/Model/MediumModel.php @@ -104,7 +104,7 @@ public function save($path = null): int $resultPath = ''; - /* + /** * If we have a (new) path and an old path, we want to move an existing * file or folder. This must be done before updating the content of a file, * if also requested (see below). From bdeb8e57f480b83cc4669ea6ee453b0e16cc95a6 Mon Sep 17 00:00:00 2001 From: Allon Moritz Date: Thu, 21 Oct 2021 14:36:05 +0200 Subject: [PATCH 34/57] Update api/components/com_media/src/View/Media/JsonapiView.php Co-authored-by: Phil E. Taylor --- api/components/com_media/src/View/Media/JsonapiView.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/components/com_media/src/View/Media/JsonapiView.php b/api/components/com_media/src/View/Media/JsonapiView.php index fd4d861417242..bd9360ec748f3 100644 --- a/api/components/com_media/src/View/Media/JsonapiView.php +++ b/api/components/com_media/src/View/Media/JsonapiView.php @@ -73,7 +73,7 @@ class JsonapiView extends BaseApiView 'adapter', 'content', 'url', - 'tempUrl' + 'tempUrl', ]; /** From 0b1526e623b3e13b2ae593d7eee0b8d62a8ce791 Mon Sep 17 00:00:00 2001 From: Allon Moritz Date: Thu, 21 Oct 2021 14:36:13 +0200 Subject: [PATCH 35/57] Update libraries/src/Error/JsonApi/SaveExceptionHandler.php Co-authored-by: Phil E. Taylor --- libraries/src/Error/JsonApi/SaveExceptionHandler.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/src/Error/JsonApi/SaveExceptionHandler.php b/libraries/src/Error/JsonApi/SaveExceptionHandler.php index a30cab0f68de1..a8ae0a15dffef 100644 --- a/libraries/src/Error/JsonApi/SaveExceptionHandler.php +++ b/libraries/src/Error/JsonApi/SaveExceptionHandler.php @@ -55,7 +55,7 @@ public function handle(Exception $e) $error = [ 'title' => $e->getMessage(), - 'code' => $status + 'code' => $status, ]; return new ResponseBag($status, [$error]); From 444547216e0e2a930a117ed97ea2b5841dd13ad2 Mon Sep 17 00:00:00 2001 From: Allon Moritz Date: Thu, 21 Oct 2021 14:37:04 +0200 Subject: [PATCH 36/57] Update api/components/com_media/src/Controller/MediaController.php Co-authored-by: Phil E. Taylor --- api/components/com_media/src/Controller/MediaController.php | 1 + 1 file changed, 1 insertion(+) diff --git a/api/components/com_media/src/Controller/MediaController.php b/api/components/com_media/src/Controller/MediaController.php index daab209882a44..0ec55ebbed0b1 100644 --- a/api/components/com_media/src/Controller/MediaController.php +++ b/api/components/com_media/src/Controller/MediaController.php @@ -133,6 +133,7 @@ public function displayList() if ($doSearch = array_key_exists('search', $apiFilterInfo)) { $this->modelState->set('search', $filter->clean($apiFilterInfo['search'], 'STRING')); + // Tell model to search recursively $this->modelState->set('search_recursive', $this->input->get('search_recursive', false, 'BOOLEAN')); } From 4c22899422e549d7698f4300e45431142876ef4f Mon Sep 17 00:00:00 2001 From: Allon Moritz Date: Thu, 21 Oct 2021 14:38:12 +0200 Subject: [PATCH 37/57] Update api/components/com_media/src/Controller/MediaController.php Co-authored-by: Phil E. Taylor --- api/components/com_media/src/Controller/MediaController.php | 1 + 1 file changed, 1 insertion(+) diff --git a/api/components/com_media/src/Controller/MediaController.php b/api/components/com_media/src/Controller/MediaController.php index 0ec55ebbed0b1..9d26c58f563e0 100644 --- a/api/components/com_media/src/Controller/MediaController.php +++ b/api/components/com_media/src/Controller/MediaController.php @@ -228,6 +228,7 @@ public function add(): void } $this->modelState->set('path', $this->input->json->get('path', '', 'STRING')); + // Check if an existing file may be overwritten. Defaults to false. $this->modelState->set('override', $this->input->json->get('override', false)); From f5698915d1a5ff28ad318f9249bf75e16546ccfe Mon Sep 17 00:00:00 2001 From: Allon Moritz Date: Thu, 21 Oct 2021 14:39:20 +0200 Subject: [PATCH 38/57] Update api/components/com_media/src/Model/AdaptersModel.php Co-authored-by: Phil E. Taylor --- api/components/com_media/src/Model/AdaptersModel.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/components/com_media/src/Model/AdaptersModel.php b/api/components/com_media/src/Model/AdaptersModel.php index ef8e8bca6aef7..5a267de1ac25a 100644 --- a/api/components/com_media/src/Model/AdaptersModel.php +++ b/api/components/com_media/src/Model/AdaptersModel.php @@ -30,7 +30,7 @@ class AdaptersModel extends BaseModel implements ListModelInterface * A hacky way to enable the standard jsonapiView::displayList() to create a Pagination object, * since com_media's ApiModel does not support pagination as we know from regular ListModel derived models. * - * @var int + * @var int * @since __DEPLOY_VERSION__ */ private $total = 0; From 54658072ac86f06d01315a6135b9bc0af87d055f Mon Sep 17 00:00:00 2001 From: Allon Moritz Date: Thu, 21 Oct 2021 14:39:31 +0200 Subject: [PATCH 39/57] Update api/components/com_media/src/Model/MediaModel.php Co-authored-by: Phil E. Taylor --- api/components/com_media/src/Model/MediaModel.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/components/com_media/src/Model/MediaModel.php b/api/components/com_media/src/Model/MediaModel.php index 8d2a0f35d348b..75e7667bfd949 100644 --- a/api/components/com_media/src/Model/MediaModel.php +++ b/api/components/com_media/src/Model/MediaModel.php @@ -66,7 +66,7 @@ public function getItems(): array 'temp' => $this->getState('temp', false), 'search' => $this->getState('search', ''), 'recursive' => $this->getState('search_recursive', false), - 'content' => $this->getState('content', false) + 'content' => $this->getState('content', false), ]; ['adapter' => $adapterName, 'path' => $path] = MediaHelper::adapterNameAndPath($this->getState('path', '')); From 84c32d5f1091b59d118ff52b0ef4e19ccbb1cb23 Mon Sep 17 00:00:00 2001 From: Allon Moritz Date: Thu, 21 Oct 2021 14:48:09 +0200 Subject: [PATCH 40/57] docs --- .../src/Controller/MediaController.php | 740 +++++++++--------- 1 file changed, 363 insertions(+), 377 deletions(-) diff --git a/api/components/com_media/src/Controller/MediaController.php b/api/components/com_media/src/Controller/MediaController.php index 9d26c58f563e0..641a42e802692 100644 --- a/api/components/com_media/src/Controller/MediaController.php +++ b/api/components/com_media/src/Controller/MediaController.php @@ -30,381 +30,367 @@ */ class MediaController extends ApiController { - use AdapterTrait; - - /** - * The content type of the item. - * - * @var string - * @since __DEPLOY_VERSION__ - */ - protected $contentType = 'media'; - - /** - * Query parameters => model state mappings - * - * @var array - * @since __DEPLOY_VERSION__ - */ - private static $listQueryModelStateMap = [ - 'path' => [ - 'name' => 'path', - 'type' => 'STRING' - ], - 'url' => [ - 'name' => 'url', - 'type' => 'BOOLEAN' - ], - 'temp' => [ - 'name' => 'temp', - 'type' => 'BOOLEAN' - ], - 'content' => [ - 'name' => 'content', - 'type' => 'BOOLEAN' - ], - ]; - - /** - * Item query parameters => model state mappings - * - * @var array - * @since __DEPLOY_VERSION__ - */ - private static $itemQueryModelStateMap = [ - 'path' => [ - 'name' => 'path', - 'type' => 'STRING' - ], - 'url' => [ - 'name' => 'url', - 'type' => 'BOOLEAN' - ], - 'temp' => [ - 'name' => 'temp', - 'type' => 'BOOLEAN' - ], - 'content' => [ - 'name' => 'content', - 'type' => 'BOOLEAN' - ], - ]; - - /** - * The default view for the display method. - * - * @var string - * - * @since __DEPLOY_VERSION__ - */ - protected $default_view = 'media'; - - /** - * Display a list of files and/or folders. - * - * @return static A \JControllerLegacy object to support chaining. - * - * @throws \Exception - * - * @since __DEPLOY_VERSION__ - */ - public function displayList() - { - // Set list specific request parameters in model state. - $this->setModelState(self::$listQueryModelStateMap); - - // Display files in specific path. - if ($this->input->exists('path')) - { - $this->modelState->set('path', $this->input->get('path', '', 'STRING')); - } - - // Return files (not folders) as urls. - if ($this->input->exists('url')) - { - $this->modelState->set('url', $this->input->get('url', true, 'BOOLEAN')); - } - - // Map JSON:API compliant filter[search] to com_media model state. - $apiFilterInfo = $this->input->get('filter', [], 'array'); - $filter = InputFilter::getInstance(); - - // Search for files matching (part of) a name or glob pattern. - if ($doSearch = array_key_exists('search', $apiFilterInfo)) - { - $this->modelState->set('search', $filter->clean($apiFilterInfo['search'], 'STRING')); - - // Tell model to search recursively - $this->modelState->set('search_recursive', $this->input->get('search_recursive', false, 'BOOLEAN')); - } - - return parent::displayList(); - } - - /** - * Display one specific file or folder. - * - * @param string $path The path of the file to display. Leave empty if you want to retrieve data from the request. - * - * @return static A \JControllerLegacy object to support chaining. - * - * @throws InvalidPathException - * @throws \Exception - * - * @since __DEPLOY_VERSION__ - */ - public function displayItem($path = '') - { - // Set list specific request parameters in model state. - $this->setModelState(self::$itemQueryModelStateMap); - - // Display files in specific path. - $this->modelState->set('path', $path ?: $this->input->get('path', '', 'STRING')); - - // Return files (not folders) as urls. - if ($this->input->exists('url')) - { - $this->modelState->set('url', $this->input->get('url', true, 'BOOLEAN')); - } - - return parent::displayItem(); - } - - /** - * Set model state using a list of mappings between query parameters and model state names. - * - * @param array $mappings A list of mappings between query parameters and model state names.. - * - * @return void - * - * @since __DEPLOY_VERSION__ - */ - private function setModelState(array $mappings): void - { - foreach ($mappings as $queryName => $modelState) - { - if ($this->input->exists($queryName)) - { - $this->modelState->set($modelState['name'], $this->input->get($queryName, '', $modelState['type'])); - } - } - } - - /** - * Method to add a new file or folder. - * - * @return void - * - * @throws FileExistsException - * @throws InvalidPathException - * @throws InvalidParameterException - * @throws \RuntimeException - * @throws \Exception - * - * @since __DEPLOY_VERSION__ - */ - public function add(): void - { - $path = $this->input->json->get('path', '', 'STRING'); - $content = $this->input->json->get('content', '', 'RAW'); - - $missingParameters = []; - - if (empty($path)) - { - $missingParameters[] = 'path'; - } - - if (empty($content)) - { - $missingParameters[] = 'content'; - } - - if (\count($missingParameters)) - { - throw new InvalidParameterException( - Text::sprintf('WEBSERVICE_COM_MEDIA_MISSING_REQUIRED_PARAMETERS', implode(' & ', $missingParameters)) - ); - } - - $this->modelState->set('path', $this->input->json->get('path', '', 'STRING')); - - // Check if an existing file may be overwritten. Defaults to false. - $this->modelState->set('override', $this->input->json->get('override', false)); - - parent::add(); - } - - /** - * Method to check if it's allowed to add a new file or folder - * - * @param array $data An array of input data. - * - * @return boolean - * - * @since __DEPLOY_VERSION__ - */ - protected function allowAdd($data = array()): bool - { - $user = $this->app->getIdentity(); - - return $user->authorise('core.create', 'com_media'); - } - - /** - * Method to modify an existing file or folder. - * - * @return void - * - * @throws FileExistsException - * @throws InvalidPathException - * @throws \RuntimeException - * @throws \Exception - * - * @since __DEPLOY_VERSION__ - */ - public function edit(): void - { - // Access check. - if (!$this->allowEdit()) - { - throw new NotAllowed('JLIB_APPLICATION_ERROR_CREATE_RECORD_NOT_PERMITTED', 403); - } - - $path = $this->input->json->get('path', '', 'STRING'); - $content = $this->input->json->get('content', '', 'RAW'); - - if (empty($path) && empty($content)) - { - throw new InvalidParameterException( - Text::sprintf('WEBSERVICE_COM_MEDIA_MISSING_REQUIRED_PARAMETERS', 'path | content') - ); - } - - $this->modelState->set('path', $this->input->json->get('path', '', 'STRING')); - // For renaming/moving files, we need the path to the existing file or folder. - $this->modelState->set('old_path', $this->input->get('path', '', 'STRING')); - // Check if an existing file may be overwritten. Defaults to true. - $this->modelState->set('override', $this->input->json->get('override', false)); - - $recordId = $this->save(); - - $this->displayItem($recordId); - } - - /** - * Method to check if it's allowed to modify an existing file or folder. - * - * @param array $data An array of input data. - * - * @return boolean - * - * @since __DEPLOY_VERSION__ - */ - protected function allowEdit($data = array(), $key = 'id'): bool - { - $user = $this->app->getIdentity(); - - // com_media's access rules contains no specific update rule. - return $user->authorise('core.edit', 'com_media'); - } - - /** - * Method to create or modify a file or folder. - * - * @param integer $recordKey The primary key of the item (if exists) - * - * @return integer The record ID on success, false on failure - * - * @since __DEPLOY_VERSION__ - */ - protected function save($recordKey = null): int - { - // Explicitly get the single item model name. - $modelName = $this->input->get('model', Inflector::singularize($this->contentType)); - $model = $this->getModel($modelName, '', ['ignore_request' => true, 'state' => $this->modelState]); - - $json = $this->input->json; - - // Split destination path into adapter name and file path. - ['adapter' => $adapter, 'path' => $path] = MediaHelper::adapterNameAndPath($this->input->get('path', '', 'STRING')); - - // Decode content, if any - if ($content = base64_decode($json->get('content', '', 'raw'))) - { - $this->checkContent(); - } - - // If there is no content, com_media assumes the path refers to a folder. - $this->modelState->set('content', $content); - - return $model->save(); - } - - /** - * Performs various checks to see if it is allowed to save the content. - * - * @return void - * - * @throws \RuntimeException - * - * @since __DEPLOY_VERSION__ - */ - private function checkContent(): void - { - $params = ComponentHelper::getParams('com_media'); - $helper = new \Joomla\CMS\Helper\MediaHelper(); - $serverlength = $this->input->server->getInt('CONTENT_LENGTH'); - - // Check if the size of the request body does not exceed various server imposed limits. - if (($params->get('upload_maxsize', 0) > 0 && $serverlength > ($params->get('upload_maxsize', 0) * 1024 * 1024)) - || $serverlength > $helper->toBytes(ini_get('upload_max_filesize')) - || $serverlength > $helper->toBytes(ini_get('post_max_size')) - || $serverlength > $helper->toBytes(ini_get('memory_limit'))) - { - throw new \RuntimeException(Text::_('COM_MEDIA_ERROR_WARNFILETOOLARGE'), 400); - } - } - - /** - * Method to delete an existing file or folder. - * - * @return void - * - * @throws InvalidPathException - * @throws \RuntimeException - * @throws \Exception - * - * @since __DEPLOY_VERSION__ - */ - public function delete($id = null): void - { - if (!$this->allowDelete()) - { - throw new NotAllowed('JLIB_APPLICATION_ERROR_DELETE_NOT_PERMITTED', 403); - } - - $this->modelState->set('path', $this->input->get('path', '', 'STRING')); - - $modelName = $this->input->get('model', Inflector::singularize($this->contentType)); - $model = $this->getModel($modelName, '', ['ignore_request' => true, 'state' => $this->modelState]); - - $model->delete(); - - $this->app->setHeader('status', 204); - } - - /** - * Method to check if it's allowed to delete an existing file or folder. - * - * @return boolean - * - * @since __DEPLOY_VERSION__ - */ - protected function allowDelete(): bool - { - $user = $this->app->getIdentity(); - - return $user->authorise('core.delete', 'com_media'); - } + use AdapterTrait; + + /** + * The content type of the item. + * + * @var string + * @since __DEPLOY_VERSION__ + */ + protected $contentType = 'media'; + + /** + * Query parameters => model state mappings + * + * @var array + * @since __DEPLOY_VERSION__ + */ + private static $listQueryModelStateMap = [ + 'path' => [ + 'name' => 'path', + 'type' => 'STRING' + ], + 'url' => [ + 'name' => 'url', + 'type' => 'BOOLEAN' + ], + 'temp' => [ + 'name' => 'temp', + 'type' => 'BOOLEAN' + ], + 'content' => [ + 'name' => 'content', + 'type' => 'BOOLEAN' + ], + ]; + + /** + * Item query parameters => model state mappings + * + * @var array + * @since __DEPLOY_VERSION__ + */ + private static $itemQueryModelStateMap = [ + 'path' => [ + 'name' => 'path', + 'type' => 'STRING' + ], + 'url' => [ + 'name' => 'url', + 'type' => 'BOOLEAN' + ], + 'temp' => [ + 'name' => 'temp', + 'type' => 'BOOLEAN' + ], + 'content' => [ + 'name' => 'content', + 'type' => 'BOOLEAN' + ], + ]; + + /** + * The default view for the display method. + * + * @var string + * + * @since __DEPLOY_VERSION__ + */ + protected $default_view = 'media'; + + /** + * Display a list of files and/or folders. + * + * @return static A \JControllerLegacy object to support chaining. + * + * @since __DEPLOY_VERSION__ + * + * @throws \Exception + */ + public function displayList() + { + // Set list specific request parameters in model state. + $this->setModelState(self::$listQueryModelStateMap); + + // Display files in specific path. + if ($this->input->exists('path')) { + $this->modelState->set('path', $this->input->get('path', '', 'STRING')); + } + + // Return files (not folders) as urls. + if ($this->input->exists('url')) { + $this->modelState->set('url', $this->input->get('url', true, 'BOOLEAN')); + } + + // Map JSON:API compliant filter[search] to com_media model state. + $apiFilterInfo = $this->input->get('filter', [], 'array'); + $filter = InputFilter::getInstance(); + + // Search for files matching (part of) a name or glob pattern. + if ($doSearch = array_key_exists('search', $apiFilterInfo)) { + $this->modelState->set('search', $filter->clean($apiFilterInfo['search'], 'STRING')); + + // Tell model to search recursively + $this->modelState->set('search_recursive', $this->input->get('search_recursive', false, 'BOOLEAN')); + } + + return parent::displayList(); + } + + /** + * Display one specific file or folder. + * + * @param string $path The path of the file to display. Leave empty if you want to retrieve data from the request. + * + * @return static A \JControllerLegacy object to support chaining. + * + * @since __DEPLOY_VERSION__ + * + * @throws InvalidPathException + * @throws \Exception + */ + public function displayItem($path = '') + { + // Set list specific request parameters in model state. + $this->setModelState(self::$itemQueryModelStateMap); + + // Display files in specific path. + $this->modelState->set('path', $path ?: $this->input->get('path', '', 'STRING')); + + // Return files (not folders) as urls. + if ($this->input->exists('url')) { + $this->modelState->set('url', $this->input->get('url', true, 'BOOLEAN')); + } + + return parent::displayItem(); + } + + /** + * Set model state using a list of mappings between query parameters and model state names. + * + * @param array $mappings A list of mappings between query parameters and model state names.. + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + private function setModelState(array $mappings): void + { + foreach ($mappings as $queryName => $modelState) { + if ($this->input->exists($queryName)) { + $this->modelState->set($modelState['name'], $this->input->get($queryName, '', $modelState['type'])); + } + } + } + + /** + * Method to add a new file or folder. + * + * @return void + * + * @since __DEPLOY_VERSION__ + * + * @throws FileExistsException + * @throws InvalidPathException + * @throws InvalidParameterException + * @throws \RuntimeException + * @throws \Exception + */ + public function add(): void + { + $path = $this->input->json->get('path', '', 'STRING'); + $content = $this->input->json->get('content', '', 'RAW'); + + $missingParameters = []; + + if (empty($path)) { + $missingParameters[] = 'path'; + } + + if (empty($content)) { + $missingParameters[] = 'content'; + } + + if (\count($missingParameters)) { + throw new InvalidParameterException( + Text::sprintf('WEBSERVICE_COM_MEDIA_MISSING_REQUIRED_PARAMETERS', implode(' & ', $missingParameters)) + ); + } + + $this->modelState->set('path', $this->input->json->get('path', '', 'STRING')); + + // Check if an existing file may be overwritten. Defaults to false. + $this->modelState->set('override', $this->input->json->get('override', false)); + + parent::add(); + } + + /** + * Method to check if it's allowed to add a new file or folder + * + * @param array $data An array of input data. + * + * @return boolean + * + * @since __DEPLOY_VERSION__ + */ + protected function allowAdd($data = array()): bool + { + $user = $this->app->getIdentity(); + + return $user->authorise('core.create', 'com_media'); + } + + /** + * Method to modify an existing file or folder. + * + * @return void + * + * @since __DEPLOY_VERSION__ + * + * @throws FileExistsException + * @throws InvalidPathException + * @throws \RuntimeException + * @throws \Exception + */ + public function edit(): void + { + // Access check. + if (!$this->allowEdit()) { + throw new NotAllowed('JLIB_APPLICATION_ERROR_CREATE_RECORD_NOT_PERMITTED', 403); + } + + $path = $this->input->json->get('path', '', 'STRING'); + $content = $this->input->json->get('content', '', 'RAW'); + + if (empty($path) && empty($content)) { + throw new InvalidParameterException( + Text::sprintf('WEBSERVICE_COM_MEDIA_MISSING_REQUIRED_PARAMETERS', 'path | content') + ); + } + + $this->modelState->set('path', $this->input->json->get('path', '', 'STRING')); + // For renaming/moving files, we need the path to the existing file or folder. + $this->modelState->set('old_path', $this->input->get('path', '', 'STRING')); + // Check if an existing file may be overwritten. Defaults to true. + $this->modelState->set('override', $this->input->json->get('override', false)); + + $recordId = $this->save(); + + $this->displayItem($recordId); + } + + /** + * Method to check if it's allowed to modify an existing file or folder. + * + * @param array $data An array of input data. + * + * @return boolean + * + * @since __DEPLOY_VERSION__ + */ + protected function allowEdit($data = array(), $key = 'id'): bool + { + $user = $this->app->getIdentity(); + + // com_media's access rules contains no specific update rule. + return $user->authorise('core.edit', 'com_media'); + } + + /** + * Method to create or modify a file or folder. + * + * @param int $recordKey The primary key of the item (if exists) + * + * @return int The record ID on success, false on failure + * + * @since __DEPLOY_VERSION__ + */ + protected function save($recordKey = null): int + { + // Explicitly get the single item model name. + $modelName = $this->input->get('model', Inflector::singularize($this->contentType)); + $model = $this->getModel($modelName, '', ['ignore_request' => true, 'state' => $this->modelState]); + + $json = $this->input->json; + + // Split destination path into adapter name and file path. + ['adapter' => $adapter, 'path' => $path] = MediaHelper::adapterNameAndPath($this->input->get('path', '', 'STRING')); + + // Decode content, if any + if ($content = base64_decode($json->get('content', '', 'raw'))) { + $this->checkContent(); + } + + // If there is no content, com_media assumes the path refers to a folder. + $this->modelState->set('content', $content); + + return $model->save(); + } + + /** + * Performs various checks to see if it is allowed to save the content. + * + * @return void + * + * @since __DEPLOY_VERSION__ + * + * @throws \RuntimeException + */ + private function checkContent(): void + { + $params = ComponentHelper::getParams('com_media'); + $helper = new \Joomla\CMS\Helper\MediaHelper(); + $serverlength = $this->input->server->getInt('CONTENT_LENGTH'); + + // Check if the size of the request body does not exceed various server imposed limits. + if (($params->get('upload_maxsize', 0) > 0 && $serverlength > ($params->get('upload_maxsize', 0) * 1024 * 1024)) + || $serverlength > $helper->toBytes(ini_get('upload_max_filesize')) + || $serverlength > $helper->toBytes(ini_get('post_max_size')) + || $serverlength > $helper->toBytes(ini_get('memory_limit'))) { + throw new \RuntimeException(Text::_('COM_MEDIA_ERROR_WARNFILETOOLARGE'), 400); + } + } + + /** + * Method to delete an existing file or folder. + * + * @return void + * + * @since __DEPLOY_VERSION__ + * + * @throws InvalidPathException + * @throws \RuntimeException + * @throws \Exception + */ + public function delete($id = null): void + { + if (!$this->allowDelete()) { + throw new NotAllowed('JLIB_APPLICATION_ERROR_DELETE_NOT_PERMITTED', 403); + } + + $this->modelState->set('path', $this->input->get('path', '', 'STRING')); + + $modelName = $this->input->get('model', Inflector::singularize($this->contentType)); + $model = $this->getModel($modelName, '', ['ignore_request' => true, 'state' => $this->modelState]); + + $model->delete(); + + $this->app->setHeader('status', 204); + } + + /** + * Method to check if it's allowed to delete an existing file or folder. + * + * @return boolean + * + * @since __DEPLOY_VERSION__ + */ + protected function allowDelete(): bool + { + $user = $this->app->getIdentity(); + + return $user->authorise('core.delete', 'com_media'); + } } From dab0060376232398b95ae82e6e7ccc732fc3da60 Mon Sep 17 00:00:00 2001 From: Allon Moritz Date: Thu, 21 Oct 2021 14:48:59 +0200 Subject: [PATCH 41/57] Update api/components/com_media/src/View/Media/JsonapiView.php Co-authored-by: Phil E. Taylor --- api/components/com_media/src/View/Media/JsonapiView.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/components/com_media/src/View/Media/JsonapiView.php b/api/components/com_media/src/View/Media/JsonapiView.php index bd9360ec748f3..76c74efec9c2f 100644 --- a/api/components/com_media/src/View/Media/JsonapiView.php +++ b/api/components/com_media/src/View/Media/JsonapiView.php @@ -47,7 +47,7 @@ class JsonapiView extends BaseApiView 'adapter', 'content', 'url', - 'tempUrl' + 'tempUrl', ]; /** From 1569a6162c92e701dda5074519aad311c073bf04 Mon Sep 17 00:00:00 2001 From: Allon Moritz Date: Thu, 21 Oct 2021 14:50:21 +0200 Subject: [PATCH 42/57] int --- api/components/com_media/src/Model/MediaModel.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/components/com_media/src/Model/MediaModel.php b/api/components/com_media/src/Model/MediaModel.php index 75e7667bfd949..bc80700962378 100644 --- a/api/components/com_media/src/Model/MediaModel.php +++ b/api/components/com_media/src/Model/MediaModel.php @@ -109,7 +109,7 @@ public function getPagination(): Pagination * does not support pagination as we know from regular ListModel derived models, * we always start at the top. * - * @return integer The starting number of items available in the data set. + * @return int The starting number of items available in the data set. * * @since __DEPLOY_VERSION__ */ @@ -121,7 +121,7 @@ public function getStart(): int /** * Method to get the total number of items for the data set. * - * @return integer The total number of items available in the data set. + * @return int The total number of items available in the data set. * * @since __DEPLOY_VERSION__ */ From d42d544632d667765fee64df87bb97e883c78b2f Mon Sep 17 00:00:00 2001 From: Allon Moritz Date: Thu, 21 Oct 2021 15:30:22 +0200 Subject: [PATCH 43/57] cs --- .../src/Provider/ProviderManager.php | 6 ++-- .../com_media/src/Helper/AdapterTrait.php | 33 +++++++++++++++++-- .../com_media/src/Model/AdapterModel.php | 21 ++++-------- .../com_media/src/Model/AdaptersModel.php | 19 +++-------- .../com_media/src/Model/MediumModel.php | 21 +++++++++--- .../com_media/src/View/Media/JsonapiView.php | 8 ----- 6 files changed, 59 insertions(+), 49 deletions(-) diff --git a/administrator/components/com_media/src/Provider/ProviderManager.php b/administrator/components/com_media/src/Provider/ProviderManager.php index 2f7563e5ee92b..6c1405a906354 100644 --- a/administrator/components/com_media/src/Provider/ProviderManager.php +++ b/administrator/components/com_media/src/Provider/ProviderManager.php @@ -91,15 +91,13 @@ public function getAdapter($name) { list($provider, $account) = array_pad(explode('-', $name, 2), 2, null); - if ($account == null) - { + if ($account == null) { throw new \Exception('Account was not set'); } $adapters = $this->getProvider($provider)->getAdapters(); - if (!isset($adapters[$account])) - { + if (!isset($adapters[$account])) { throw new \Exception("The account was not found"); } diff --git a/api/components/com_media/src/Helper/AdapterTrait.php b/api/components/com_media/src/Helper/AdapterTrait.php index 233d393469b5e..e323a3d0cb3ab 100644 --- a/api/components/com_media/src/Helper/AdapterTrait.php +++ b/api/components/com_media/src/Helper/AdapterTrait.php @@ -15,6 +15,7 @@ use Joomla\CMS\Plugin\PluginHelper; use Joomla\Component\Media\Administrator\Adapter\AdapterInterface; use Joomla\Component\Media\Administrator\Event\MediaProviderEvent; +use Joomla\Component\Media\Administrator\Provider\ProviderInterface; use Joomla\Component\Media\Administrator\Provider\ProviderManager; /** @@ -34,7 +35,21 @@ trait AdapterTrait private $providerManager = null; /** - * Return a provider manager. + * Returns a provider for the given id. + * + * @return ProviderInterface + * + * @throws \Exception + * + * @since __DEPLOY_VERSION__ + */ + private function getProvider(String $id): ProviderInterface + { + return $this->getProviderManager()->getProvider($id); + } + + /** + * Return an adapter for the given name. * * @return AdapterInterface * @@ -42,7 +57,19 @@ trait AdapterTrait * * @since __DEPLOY_VERSION__ */ - private function getAdapter(String $name) + private function getAdapter(String $name): AdapterInterface + { + return $this->getProviderManager()->getAdapter($name); + } + + /** + * Return a provider manager. + * + * @return ProviderManager + * + * @since __DEPLOY_VERSION__ + */ + private function getProviderManager(): ProviderManager { if (!$this->providerManager) { @@ -55,6 +82,6 @@ private function getAdapter(String $name) Factory::getApplication()->triggerEvent('onSetupProviders', $event); } - return $this->providerManager->getAdapter($name); + return $this->providerManager; } } diff --git a/api/components/com_media/src/Model/AdapterModel.php b/api/components/com_media/src/Model/AdapterModel.php index 7361c32090396..381306c9bb63f 100644 --- a/api/components/com_media/src/Model/AdapterModel.php +++ b/api/components/com_media/src/Model/AdapterModel.php @@ -11,11 +11,8 @@ \defined('_JEXEC') or die; -use Joomla\CMS\Factory; use Joomla\CMS\MVC\Model\BaseModel; -use Joomla\CMS\Plugin\PluginHelper; -use Joomla\Component\Media\Administrator\Event\MediaProviderEvent; -use Joomla\Component\Media\Administrator\Provider\ProviderManager; +use Joomla\Component\Media\Api\Helper\AdapterTrait; /** * Media web service model supporting a single adapter item. @@ -24,6 +21,8 @@ */ class AdapterModel extends BaseModel { + use AdapterTrait; + /** * Method to get a single adapter. * @@ -33,14 +32,6 @@ class AdapterModel extends BaseModel */ public function getItem(): \stdClass { - $providerManager = new ProviderManager; - - // Fire the event to get the results - $eventParameters = ['context' => 'AdapterManager', 'providerManager' => $providerManager]; - $event = new MediaProviderEvent('onSetupProviders', $eventParameters); - PluginHelper::importPlugin('filesystem'); - Factory::getApplication()->getDispatcher()->dispatch('onSetupProviders', $event); - list($provider, $account) = array_pad(explode('-', $this->getState('id'), 2), 2, null); if ($account === null) @@ -48,10 +39,10 @@ public function getItem(): \stdClass throw new \Exception('Account was not set'); } - $provider = $providerManager->getProvider($provider); - $adapter = $providerManager->getAdapter($this->getState('id')); + $provider = $this->getProvider($provider); + $adapter = $this->getAdapter($this->getState('id')); - $obj = new \stdClass; + $obj = new \stdClass(); $obj->id = $provider->getID() . '-' . $adapter->getAdapterName(); $obj->provider_id = $provider->getID(); $obj->name = $adapter->getAdapterName(); diff --git a/api/components/com_media/src/Model/AdaptersModel.php b/api/components/com_media/src/Model/AdaptersModel.php index 5a267de1ac25a..351b79ee9aba0 100644 --- a/api/components/com_media/src/Model/AdaptersModel.php +++ b/api/components/com_media/src/Model/AdaptersModel.php @@ -11,13 +11,10 @@ \defined('_JEXEC') or die; -use Joomla\CMS\Factory; use Joomla\CMS\MVC\Model\BaseModel; use Joomla\CMS\MVC\Model\ListModelInterface; use Joomla\CMS\Pagination\Pagination; -use Joomla\CMS\Plugin\PluginHelper; -use Joomla\Component\Media\Administrator\Event\MediaProviderEvent; -use Joomla\Component\Media\Administrator\Provider\ProviderManager; +use Joomla\Component\Media\Api\Helper\AdapterTrait; /** * Media web service model supporting lists of media adapters. @@ -26,6 +23,8 @@ */ class AdaptersModel extends BaseModel implements ListModelInterface { + use AdapterTrait; + /** * A hacky way to enable the standard jsonapiView::displayList() to create a Pagination object, * since com_media's ApiModel does not support pagination as we know from regular ListModel derived models. @@ -44,20 +43,12 @@ class AdaptersModel extends BaseModel implements ListModelInterface */ public function getItems(): array { - $providerManager = new ProviderManager; - - // Fire the event to get the results - $eventParameters = ['context' => 'AdapterManager', 'providerManager' => $providerManager]; - $event = new MediaProviderEvent('onSetupProviders', $eventParameters); - PluginHelper::importPlugin('filesystem'); - Factory::getApplication()->getDispatcher()->dispatch('onSetupProviders', $event); - $adapters = []; - foreach($providerManager->getProviders() as $provider) + foreach ($this->getProviderManager()->getProviders() as $provider) { foreach ($provider->getAdapters() as $adapter) { - $obj = new \stdClass; + $obj = new \stdClass(); $obj->id = $provider->getID() . '-' . $adapter->getAdapterName(); $obj->provider_id = $provider->getID(); $obj->name = $adapter->getAdapterName(); diff --git a/api/components/com_media/src/Model/MediumModel.php b/api/components/com_media/src/Model/MediumModel.php index 726690d759562..f77ee934e84af 100644 --- a/api/components/com_media/src/Model/MediumModel.php +++ b/api/components/com_media/src/Model/MediumModel.php @@ -84,7 +84,7 @@ public function getItem() * * @param string $path The primary key of the item (if exists) * - * @return integer The record ID on success, false on failure + * @return int The record ID on success, false on failure * * @since __DEPLOY_VERSION__ * @throws Save @@ -123,7 +123,8 @@ public function save($path = null): int { throw new Save( Text::sprintf( - 'WEBSERVICE_COM_MEDIA_FILE_NOT_FOUND', $oldPath + 'WEBSERVICE_COM_MEDIA_FILE_NOT_FOUND', + $oldPath ), 404 ); @@ -145,10 +146,17 @@ public function save($path = null): int // Otherwise a folder is assumed. $name = $content ? $this->mediaApiModel->createFile( - $adapterName, $basename, $dirname, $content, $override + $adapterName, + $basename, + $dirname, + $content, + $override ) : $this->mediaApiModel->createFolder( - $adapterName, $basename, $dirname, $override + $adapterName, + $basename, + $dirname, + $override ); $resultPath = $dirname . '/' . $name; @@ -197,7 +205,10 @@ public function save($path = null): int try { $this->mediaApiModel->updateFile( - $adapterName, $basename, $dirname, $content + $adapterName, + $basename, + $dirname, + $content ); } catch (FileNotFoundException $e) diff --git a/api/components/com_media/src/View/Media/JsonapiView.php b/api/components/com_media/src/View/Media/JsonapiView.php index 76c74efec9c2f..84ee67bf32ede 100644 --- a/api/components/com_media/src/View/Media/JsonapiView.php +++ b/api/components/com_media/src/View/Media/JsonapiView.php @@ -76,14 +76,6 @@ class JsonapiView extends BaseApiView 'tempUrl', ]; - /** - * Holds the available media file adapters. - * - * @var ProviderManager|null - * @since __DEPLOY_VERSION__ - */ - private $providerManager = null; - /** * Prepare item before render. * From 1b3dd32cecb1a2866e91e36f0f82d6c4f5384d56 Mon Sep 17 00:00:00 2001 From: Allon Moritz Date: Thu, 21 Oct 2021 15:32:34 +0200 Subject: [PATCH 44/57] cs --- .../components/com_media/src/Provider/ProviderManager.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/administrator/components/com_media/src/Provider/ProviderManager.php b/administrator/components/com_media/src/Provider/ProviderManager.php index 6c1405a906354..2f7563e5ee92b 100644 --- a/administrator/components/com_media/src/Provider/ProviderManager.php +++ b/administrator/components/com_media/src/Provider/ProviderManager.php @@ -91,13 +91,15 @@ public function getAdapter($name) { list($provider, $account) = array_pad(explode('-', $name, 2), 2, null); - if ($account == null) { + if ($account == null) + { throw new \Exception('Account was not set'); } $adapters = $this->getProvider($provider)->getAdapters(); - if (!isset($adapters[$account])) { + if (!isset($adapters[$account])) + { throw new \Exception("The account was not found"); } From 689d1677c68437aec04b7d3448945164a7605d29 Mon Sep 17 00:00:00 2001 From: Allon Moritz Date: Thu, 21 Oct 2021 15:37:59 +0200 Subject: [PATCH 45/57] more cleanup --- .../src/Controller/MediaController.php | 42 ++++++++++++------- tests/Codeception/acceptance.suite.dist.yml | 2 +- .../api/com_banners/BannerCest.php | 2 +- .../api/com_contact/ContactCest.php | 2 +- .../api/com_content/ContentCest.php | 2 +- 5 files changed, 32 insertions(+), 18 deletions(-) diff --git a/api/components/com_media/src/Controller/MediaController.php b/api/components/com_media/src/Controller/MediaController.php index 641a42e802692..c8cd549da66b6 100644 --- a/api/components/com_media/src/Controller/MediaController.php +++ b/api/components/com_media/src/Controller/MediaController.php @@ -114,12 +114,14 @@ public function displayList() $this->setModelState(self::$listQueryModelStateMap); // Display files in specific path. - if ($this->input->exists('path')) { + if ($this->input->exists('path')) + { $this->modelState->set('path', $this->input->get('path', '', 'STRING')); } // Return files (not folders) as urls. - if ($this->input->exists('url')) { + if ($this->input->exists('url')) + { $this->modelState->set('url', $this->input->get('url', true, 'BOOLEAN')); } @@ -128,7 +130,8 @@ public function displayList() $filter = InputFilter::getInstance(); // Search for files matching (part of) a name or glob pattern. - if ($doSearch = array_key_exists('search', $apiFilterInfo)) { + if ($doSearch = array_key_exists('search', $apiFilterInfo)) + { $this->modelState->set('search', $filter->clean($apiFilterInfo['search'], 'STRING')); // Tell model to search recursively @@ -159,7 +162,8 @@ public function displayItem($path = '') $this->modelState->set('path', $path ?: $this->input->get('path', '', 'STRING')); // Return files (not folders) as urls. - if ($this->input->exists('url')) { + if ($this->input->exists('url')) + { $this->modelState->set('url', $this->input->get('url', true, 'BOOLEAN')); } @@ -177,8 +181,10 @@ public function displayItem($path = '') */ private function setModelState(array $mappings): void { - foreach ($mappings as $queryName => $modelState) { - if ($this->input->exists($queryName)) { + foreach ($mappings as $queryName => $modelState) + { + if ($this->input->exists($queryName)) + { $this->modelState->set($modelState['name'], $this->input->get($queryName, '', $modelState['type'])); } } @@ -204,15 +210,18 @@ public function add(): void $missingParameters = []; - if (empty($path)) { + if (empty($path)) + { $missingParameters[] = 'path'; } - if (empty($content)) { + if (empty($content)) + { $missingParameters[] = 'content'; } - if (\count($missingParameters)) { + if (\count($missingParameters)) + { throw new InvalidParameterException( Text::sprintf('WEBSERVICE_COM_MEDIA_MISSING_REQUIRED_PARAMETERS', implode(' & ', $missingParameters)) ); @@ -257,14 +266,16 @@ protected function allowAdd($data = array()): bool public function edit(): void { // Access check. - if (!$this->allowEdit()) { + if (!$this->allowEdit()) + { throw new NotAllowed('JLIB_APPLICATION_ERROR_CREATE_RECORD_NOT_PERMITTED', 403); } $path = $this->input->json->get('path', '', 'STRING'); $content = $this->input->json->get('content', '', 'RAW'); - if (empty($path) && empty($content)) { + if (empty($path) && empty($content)) + { throw new InvalidParameterException( Text::sprintf('WEBSERVICE_COM_MEDIA_MISSING_REQUIRED_PARAMETERS', 'path | content') ); @@ -319,7 +330,8 @@ protected function save($recordKey = null): int ['adapter' => $adapter, 'path' => $path] = MediaHelper::adapterNameAndPath($this->input->get('path', '', 'STRING')); // Decode content, if any - if ($content = base64_decode($json->get('content', '', 'raw'))) { + if ($content = base64_decode($json->get('content', '', 'raw'))) + { $this->checkContent(); } @@ -348,7 +360,8 @@ private function checkContent(): void if (($params->get('upload_maxsize', 0) > 0 && $serverlength > ($params->get('upload_maxsize', 0) * 1024 * 1024)) || $serverlength > $helper->toBytes(ini_get('upload_max_filesize')) || $serverlength > $helper->toBytes(ini_get('post_max_size')) - || $serverlength > $helper->toBytes(ini_get('memory_limit'))) { + || $serverlength > $helper->toBytes(ini_get('memory_limit'))) + { throw new \RuntimeException(Text::_('COM_MEDIA_ERROR_WARNFILETOOLARGE'), 400); } } @@ -366,7 +379,8 @@ private function checkContent(): void */ public function delete($id = null): void { - if (!$this->allowDelete()) { + if (!$this->allowDelete()) + { throw new NotAllowed('JLIB_APPLICATION_ERROR_DELETE_NOT_PERMITTED', 403); } diff --git a/tests/Codeception/acceptance.suite.dist.yml b/tests/Codeception/acceptance.suite.dist.yml index b7d67a979f639..482b16c72daec 100644 --- a/tests/Codeception/acceptance.suite.dist.yml +++ b/tests/Codeception/acceptance.suite.dist.yml @@ -17,7 +17,7 @@ modules: window_size: 1920x1080 capabilities: 'goog:chromeOptions': - args: ["whitelisted-ips", "disable-gpu", "no-sandbox", "window-size=1920x1080", "--disable-dev-shm-usage"] + args: ["headless", whitelisted-ips", "disable-gpu", "no-sandbox", "window-size=1920x1080", "--disable-dev-shm-usage"] name: 'jane doe' # Name for the Administrator username: 'ci-admin' # UserName for the Administrator password: 'joomla-17082005' # Password for the Administrator diff --git a/tests/Codeception/api/com_banners/BannerCest.php b/tests/Codeception/api/com_banners/BannerCest.php index cf03b48f5faf6..3eb10f39fb6e5 100644 --- a/tests/Codeception/api/com_banners/BannerCest.php +++ b/tests/Codeception/api/com_banners/BannerCest.php @@ -25,7 +25,7 @@ class BannerCest * * @return void * - * @since __DEPLOY_VERSION__ + * @since 4.0.0 */ public function _before(ApiTester $I) { diff --git a/tests/Codeception/api/com_contact/ContactCest.php b/tests/Codeception/api/com_contact/ContactCest.php index b3c179db0b5ee..ce3f93e865d66 100644 --- a/tests/Codeception/api/com_contact/ContactCest.php +++ b/tests/Codeception/api/com_contact/ContactCest.php @@ -25,7 +25,7 @@ class ContactCest * * @return void * - * @since __DEPLOY_VERSION__ + * @since 4.0.0 */ public function _before(ApiTester $I) { diff --git a/tests/Codeception/api/com_content/ContentCest.php b/tests/Codeception/api/com_content/ContentCest.php index 0896e0dfbc601..9a75b6cc35e79 100644 --- a/tests/Codeception/api/com_content/ContentCest.php +++ b/tests/Codeception/api/com_content/ContentCest.php @@ -25,7 +25,7 @@ class ContentCest * * @return void * - * @since __DEPLOY_VERSION__ + * @since 4.0.0 */ public function _before(ApiTester $I) { From dd756fda02815d8f1e9e82caaa0a980c4cd9051b Mon Sep 17 00:00:00 2001 From: Allon Moritz Date: Thu, 21 Oct 2021 15:41:49 +0200 Subject: [PATCH 46/57] tabs --- .../src/Controller/MediaController.php | 700 +++++++++--------- 1 file changed, 350 insertions(+), 350 deletions(-) diff --git a/api/components/com_media/src/Controller/MediaController.php b/api/components/com_media/src/Controller/MediaController.php index c8cd549da66b6..cf7c66b2c16d1 100644 --- a/api/components/com_media/src/Controller/MediaController.php +++ b/api/components/com_media/src/Controller/MediaController.php @@ -30,381 +30,381 @@ */ class MediaController extends ApiController { - use AdapterTrait; - - /** - * The content type of the item. - * - * @var string - * @since __DEPLOY_VERSION__ - */ - protected $contentType = 'media'; - - /** - * Query parameters => model state mappings - * - * @var array - * @since __DEPLOY_VERSION__ - */ - private static $listQueryModelStateMap = [ - 'path' => [ - 'name' => 'path', - 'type' => 'STRING' - ], - 'url' => [ - 'name' => 'url', - 'type' => 'BOOLEAN' - ], - 'temp' => [ - 'name' => 'temp', - 'type' => 'BOOLEAN' - ], - 'content' => [ - 'name' => 'content', - 'type' => 'BOOLEAN' - ], - ]; - - /** - * Item query parameters => model state mappings - * - * @var array - * @since __DEPLOY_VERSION__ - */ - private static $itemQueryModelStateMap = [ - 'path' => [ - 'name' => 'path', - 'type' => 'STRING' - ], - 'url' => [ - 'name' => 'url', - 'type' => 'BOOLEAN' - ], - 'temp' => [ - 'name' => 'temp', - 'type' => 'BOOLEAN' - ], - 'content' => [ - 'name' => 'content', - 'type' => 'BOOLEAN' - ], - ]; - - /** - * The default view for the display method. - * - * @var string - * - * @since __DEPLOY_VERSION__ - */ - protected $default_view = 'media'; - - /** - * Display a list of files and/or folders. - * - * @return static A \JControllerLegacy object to support chaining. - * - * @since __DEPLOY_VERSION__ - * - * @throws \Exception - */ - public function displayList() - { - // Set list specific request parameters in model state. - $this->setModelState(self::$listQueryModelStateMap); - - // Display files in specific path. - if ($this->input->exists('path')) + use AdapterTrait; + + /** + * The content type of the item. + * + * @var string + * @since __DEPLOY_VERSION__ + */ + protected $contentType = 'media'; + + /** + * Query parameters => model state mappings + * + * @var array + * @since __DEPLOY_VERSION__ + */ + private static $listQueryModelStateMap = [ + 'path' => [ + 'name' => 'path', + 'type' => 'STRING' + ], + 'url' => [ + 'name' => 'url', + 'type' => 'BOOLEAN' + ], + 'temp' => [ + 'name' => 'temp', + 'type' => 'BOOLEAN' + ], + 'content' => [ + 'name' => 'content', + 'type' => 'BOOLEAN' + ], + ]; + + /** + * Item query parameters => model state mappings + * + * @var array + * @since __DEPLOY_VERSION__ + */ + private static $itemQueryModelStateMap = [ + 'path' => [ + 'name' => 'path', + 'type' => 'STRING' + ], + 'url' => [ + 'name' => 'url', + 'type' => 'BOOLEAN' + ], + 'temp' => [ + 'name' => 'temp', + 'type' => 'BOOLEAN' + ], + 'content' => [ + 'name' => 'content', + 'type' => 'BOOLEAN' + ], + ]; + + /** + * The default view for the display method. + * + * @var string + * + * @since __DEPLOY_VERSION__ + */ + protected $default_view = 'media'; + + /** + * Display a list of files and/or folders. + * + * @return static A \JControllerLegacy object to support chaining. + * + * @since __DEPLOY_VERSION__ + * + * @throws \Exception + */ + public function displayList() + { + // Set list specific request parameters in model state. + $this->setModelState(self::$listQueryModelStateMap); + + // Display files in specific path. + if ($this->input->exists('path')) { - $this->modelState->set('path', $this->input->get('path', '', 'STRING')); - } + $this->modelState->set('path', $this->input->get('path', '', 'STRING')); + } - // Return files (not folders) as urls. - if ($this->input->exists('url')) + // Return files (not folders) as urls. + if ($this->input->exists('url')) { - $this->modelState->set('url', $this->input->get('url', true, 'BOOLEAN')); - } + $this->modelState->set('url', $this->input->get('url', true, 'BOOLEAN')); + } - // Map JSON:API compliant filter[search] to com_media model state. - $apiFilterInfo = $this->input->get('filter', [], 'array'); - $filter = InputFilter::getInstance(); + // Map JSON:API compliant filter[search] to com_media model state. + $apiFilterInfo = $this->input->get('filter', [], 'array'); + $filter = InputFilter::getInstance(); - // Search for files matching (part of) a name or glob pattern. - if ($doSearch = array_key_exists('search', $apiFilterInfo)) + // Search for files matching (part of) a name or glob pattern. + if ($doSearch = array_key_exists('search', $apiFilterInfo)) { - $this->modelState->set('search', $filter->clean($apiFilterInfo['search'], 'STRING')); - - // Tell model to search recursively - $this->modelState->set('search_recursive', $this->input->get('search_recursive', false, 'BOOLEAN')); - } - - return parent::displayList(); - } - - /** - * Display one specific file or folder. - * - * @param string $path The path of the file to display. Leave empty if you want to retrieve data from the request. - * - * @return static A \JControllerLegacy object to support chaining. - * - * @since __DEPLOY_VERSION__ - * - * @throws InvalidPathException - * @throws \Exception - */ - public function displayItem($path = '') - { - // Set list specific request parameters in model state. - $this->setModelState(self::$itemQueryModelStateMap); - - // Display files in specific path. - $this->modelState->set('path', $path ?: $this->input->get('path', '', 'STRING')); - - // Return files (not folders) as urls. - if ($this->input->exists('url')) + $this->modelState->set('search', $filter->clean($apiFilterInfo['search'], 'STRING')); + + // Tell model to search recursively + $this->modelState->set('search_recursive', $this->input->get('search_recursive', false, 'BOOLEAN')); + } + + return parent::displayList(); + } + + /** + * Display one specific file or folder. + * + * @param string $path The path of the file to display. Leave empty if you want to retrieve data from the request. + * + * @return static A \JControllerLegacy object to support chaining. + * + * @since __DEPLOY_VERSION__ + * + * @throws InvalidPathException + * @throws \Exception + */ + public function displayItem($path = '') + { + // Set list specific request parameters in model state. + $this->setModelState(self::$itemQueryModelStateMap); + + // Display files in specific path. + $this->modelState->set('path', $path ?: $this->input->get('path', '', 'STRING')); + + // Return files (not folders) as urls. + if ($this->input->exists('url')) { - $this->modelState->set('url', $this->input->get('url', true, 'BOOLEAN')); - } - - return parent::displayItem(); - } - - /** - * Set model state using a list of mappings between query parameters and model state names. - * - * @param array $mappings A list of mappings between query parameters and model state names.. - * - * @return void - * - * @since __DEPLOY_VERSION__ - */ - private function setModelState(array $mappings): void - { - foreach ($mappings as $queryName => $modelState) + $this->modelState->set('url', $this->input->get('url', true, 'BOOLEAN')); + } + + return parent::displayItem(); + } + + /** + * Set model state using a list of mappings between query parameters and model state names. + * + * @param array $mappings A list of mappings between query parameters and model state names.. + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + private function setModelState(array $mappings): void + { + foreach ($mappings as $queryName => $modelState) { - if ($this->input->exists($queryName)) + if ($this->input->exists($queryName)) { - $this->modelState->set($modelState['name'], $this->input->get($queryName, '', $modelState['type'])); - } - } - } - - /** - * Method to add a new file or folder. - * - * @return void - * - * @since __DEPLOY_VERSION__ - * - * @throws FileExistsException - * @throws InvalidPathException - * @throws InvalidParameterException - * @throws \RuntimeException - * @throws \Exception - */ - public function add(): void - { - $path = $this->input->json->get('path', '', 'STRING'); - $content = $this->input->json->get('content', '', 'RAW'); - - $missingParameters = []; - - if (empty($path)) + $this->modelState->set($modelState['name'], $this->input->get($queryName, '', $modelState['type'])); + } + } + } + + /** + * Method to add a new file or folder. + * + * @return void + * + * @since __DEPLOY_VERSION__ + * + * @throws FileExistsException + * @throws InvalidPathException + * @throws InvalidParameterException + * @throws \RuntimeException + * @throws \Exception + */ + public function add(): void + { + $path = $this->input->json->get('path', '', 'STRING'); + $content = $this->input->json->get('content', '', 'RAW'); + + $missingParameters = []; + + if (empty($path)) { - $missingParameters[] = 'path'; - } + $missingParameters[] = 'path'; + } - if (empty($content)) + if (empty($content)) { - $missingParameters[] = 'content'; - } + $missingParameters[] = 'content'; + } - if (\count($missingParameters)) + if (\count($missingParameters)) { - throw new InvalidParameterException( - Text::sprintf('WEBSERVICE_COM_MEDIA_MISSING_REQUIRED_PARAMETERS', implode(' & ', $missingParameters)) - ); - } - - $this->modelState->set('path', $this->input->json->get('path', '', 'STRING')); - - // Check if an existing file may be overwritten. Defaults to false. - $this->modelState->set('override', $this->input->json->get('override', false)); - - parent::add(); - } - - /** - * Method to check if it's allowed to add a new file or folder - * - * @param array $data An array of input data. - * - * @return boolean - * - * @since __DEPLOY_VERSION__ - */ - protected function allowAdd($data = array()): bool - { - $user = $this->app->getIdentity(); - - return $user->authorise('core.create', 'com_media'); - } - - /** - * Method to modify an existing file or folder. - * - * @return void - * - * @since __DEPLOY_VERSION__ - * - * @throws FileExistsException - * @throws InvalidPathException - * @throws \RuntimeException - * @throws \Exception - */ - public function edit(): void - { - // Access check. - if (!$this->allowEdit()) + throw new InvalidParameterException( + Text::sprintf('WEBSERVICE_COM_MEDIA_MISSING_REQUIRED_PARAMETERS', implode(' & ', $missingParameters)) + ); + } + + $this->modelState->set('path', $this->input->json->get('path', '', 'STRING')); + + // Check if an existing file may be overwritten. Defaults to false. + $this->modelState->set('override', $this->input->json->get('override', false)); + + parent::add(); + } + + /** + * Method to check if it's allowed to add a new file or folder + * + * @param array $data An array of input data. + * + * @return boolean + * + * @since __DEPLOY_VERSION__ + */ + protected function allowAdd($data = array()): bool + { + $user = $this->app->getIdentity(); + + return $user->authorise('core.create', 'com_media'); + } + + /** + * Method to modify an existing file or folder. + * + * @return void + * + * @since __DEPLOY_VERSION__ + * + * @throws FileExistsException + * @throws InvalidPathException + * @throws \RuntimeException + * @throws \Exception + */ + public function edit(): void + { + // Access check. + if (!$this->allowEdit()) { - throw new NotAllowed('JLIB_APPLICATION_ERROR_CREATE_RECORD_NOT_PERMITTED', 403); - } + throw new NotAllowed('JLIB_APPLICATION_ERROR_CREATE_RECORD_NOT_PERMITTED', 403); + } - $path = $this->input->json->get('path', '', 'STRING'); - $content = $this->input->json->get('content', '', 'RAW'); + $path = $this->input->json->get('path', '', 'STRING'); + $content = $this->input->json->get('content', '', 'RAW'); - if (empty($path) && empty($content)) + if (empty($path) && empty($content)) { - throw new InvalidParameterException( - Text::sprintf('WEBSERVICE_COM_MEDIA_MISSING_REQUIRED_PARAMETERS', 'path | content') - ); - } - - $this->modelState->set('path', $this->input->json->get('path', '', 'STRING')); - // For renaming/moving files, we need the path to the existing file or folder. - $this->modelState->set('old_path', $this->input->get('path', '', 'STRING')); - // Check if an existing file may be overwritten. Defaults to true. - $this->modelState->set('override', $this->input->json->get('override', false)); - - $recordId = $this->save(); - - $this->displayItem($recordId); - } - - /** - * Method to check if it's allowed to modify an existing file or folder. - * - * @param array $data An array of input data. - * - * @return boolean - * - * @since __DEPLOY_VERSION__ - */ - protected function allowEdit($data = array(), $key = 'id'): bool - { - $user = $this->app->getIdentity(); - - // com_media's access rules contains no specific update rule. - return $user->authorise('core.edit', 'com_media'); - } - - /** - * Method to create or modify a file or folder. - * - * @param int $recordKey The primary key of the item (if exists) - * - * @return int The record ID on success, false on failure - * - * @since __DEPLOY_VERSION__ - */ - protected function save($recordKey = null): int - { - // Explicitly get the single item model name. - $modelName = $this->input->get('model', Inflector::singularize($this->contentType)); - $model = $this->getModel($modelName, '', ['ignore_request' => true, 'state' => $this->modelState]); - - $json = $this->input->json; - - // Split destination path into adapter name and file path. - ['adapter' => $adapter, 'path' => $path] = MediaHelper::adapterNameAndPath($this->input->get('path', '', 'STRING')); - - // Decode content, if any - if ($content = base64_decode($json->get('content', '', 'raw'))) + throw new InvalidParameterException( + Text::sprintf('WEBSERVICE_COM_MEDIA_MISSING_REQUIRED_PARAMETERS', 'path | content') + ); + } + + $this->modelState->set('path', $this->input->json->get('path', '', 'STRING')); + // For renaming/moving files, we need the path to the existing file or folder. + $this->modelState->set('old_path', $this->input->get('path', '', 'STRING')); + // Check if an existing file may be overwritten. Defaults to true. + $this->modelState->set('override', $this->input->json->get('override', false)); + + $recordId = $this->save(); + + $this->displayItem($recordId); + } + + /** + * Method to check if it's allowed to modify an existing file or folder. + * + * @param array $data An array of input data. + * + * @return boolean + * + * @since __DEPLOY_VERSION__ + */ + protected function allowEdit($data = array(), $key = 'id'): bool + { + $user = $this->app->getIdentity(); + + // com_media's access rules contains no specific update rule. + return $user->authorise('core.edit', 'com_media'); + } + + /** + * Method to create or modify a file or folder. + * + * @param int $recordKey The primary key of the item (if exists) + * + * @return int The record ID on success, false on failure + * + * @since __DEPLOY_VERSION__ + */ + protected function save($recordKey = null): int + { + // Explicitly get the single item model name. + $modelName = $this->input->get('model', Inflector::singularize($this->contentType)); + $model = $this->getModel($modelName, '', ['ignore_request' => true, 'state' => $this->modelState]); + + $json = $this->input->json; + + // Split destination path into adapter name and file path. + ['adapter' => $adapter, 'path' => $path] = MediaHelper::adapterNameAndPath($this->input->get('path', '', 'STRING')); + + // Decode content, if any + if ($content = base64_decode($json->get('content', '', 'raw'))) { - $this->checkContent(); - } - - // If there is no content, com_media assumes the path refers to a folder. - $this->modelState->set('content', $content); - - return $model->save(); - } - - /** - * Performs various checks to see if it is allowed to save the content. - * - * @return void - * - * @since __DEPLOY_VERSION__ - * - * @throws \RuntimeException - */ - private function checkContent(): void - { - $params = ComponentHelper::getParams('com_media'); - $helper = new \Joomla\CMS\Helper\MediaHelper(); - $serverlength = $this->input->server->getInt('CONTENT_LENGTH'); - - // Check if the size of the request body does not exceed various server imposed limits. - if (($params->get('upload_maxsize', 0) > 0 && $serverlength > ($params->get('upload_maxsize', 0) * 1024 * 1024)) - || $serverlength > $helper->toBytes(ini_get('upload_max_filesize')) - || $serverlength > $helper->toBytes(ini_get('post_max_size')) - || $serverlength > $helper->toBytes(ini_get('memory_limit'))) + $this->checkContent(); + } + + // If there is no content, com_media assumes the path refers to a folder. + $this->modelState->set('content', $content); + + return $model->save(); + } + + /** + * Performs various checks to see if it is allowed to save the content. + * + * @return void + * + * @since __DEPLOY_VERSION__ + * + * @throws \RuntimeException + */ + private function checkContent(): void + { + $params = ComponentHelper::getParams('com_media'); + $helper = new \Joomla\CMS\Helper\MediaHelper(); + $serverlength = $this->input->server->getInt('CONTENT_LENGTH'); + + // Check if the size of the request body does not exceed various server imposed limits. + if (($params->get('upload_maxsize', 0) > 0 && $serverlength > ($params->get('upload_maxsize', 0) * 1024 * 1024)) + || $serverlength > $helper->toBytes(ini_get('upload_max_filesize')) + || $serverlength > $helper->toBytes(ini_get('post_max_size')) + || $serverlength > $helper->toBytes(ini_get('memory_limit'))) { - throw new \RuntimeException(Text::_('COM_MEDIA_ERROR_WARNFILETOOLARGE'), 400); - } - } - - /** - * Method to delete an existing file or folder. - * - * @return void - * - * @since __DEPLOY_VERSION__ - * - * @throws InvalidPathException - * @throws \RuntimeException - * @throws \Exception - */ - public function delete($id = null): void - { - if (!$this->allowDelete()) + throw new \RuntimeException(Text::_('COM_MEDIA_ERROR_WARNFILETOOLARGE'), 400); + } + } + + /** + * Method to delete an existing file or folder. + * + * @return void + * + * @since __DEPLOY_VERSION__ + * + * @throws InvalidPathException + * @throws \RuntimeException + * @throws \Exception + */ + public function delete($id = null): void + { + if (!$this->allowDelete()) { - throw new NotAllowed('JLIB_APPLICATION_ERROR_DELETE_NOT_PERMITTED', 403); - } + throw new NotAllowed('JLIB_APPLICATION_ERROR_DELETE_NOT_PERMITTED', 403); + } - $this->modelState->set('path', $this->input->get('path', '', 'STRING')); + $this->modelState->set('path', $this->input->get('path', '', 'STRING')); - $modelName = $this->input->get('model', Inflector::singularize($this->contentType)); - $model = $this->getModel($modelName, '', ['ignore_request' => true, 'state' => $this->modelState]); + $modelName = $this->input->get('model', Inflector::singularize($this->contentType)); + $model = $this->getModel($modelName, '', ['ignore_request' => true, 'state' => $this->modelState]); - $model->delete(); + $model->delete(); - $this->app->setHeader('status', 204); - } + $this->app->setHeader('status', 204); + } - /** - * Method to check if it's allowed to delete an existing file or folder. - * - * @return boolean - * - * @since __DEPLOY_VERSION__ - */ - protected function allowDelete(): bool - { - $user = $this->app->getIdentity(); + /** + * Method to check if it's allowed to delete an existing file or folder. + * + * @return boolean + * + * @since __DEPLOY_VERSION__ + */ + protected function allowDelete(): bool + { + $user = $this->app->getIdentity(); - return $user->authorise('core.delete', 'com_media'); - } + return $user->authorise('core.delete', 'com_media'); + } } From 3bb4a666efd9308398737b63d2d87ded50463805 Mon Sep 17 00:00:00 2001 From: Allon Moritz Date: Thu, 21 Oct 2021 15:45:49 +0200 Subject: [PATCH 47/57] restore --- tests/Codeception/acceptance.suite.dist.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Codeception/acceptance.suite.dist.yml b/tests/Codeception/acceptance.suite.dist.yml index 482b16c72daec..8222ec9091571 100644 --- a/tests/Codeception/acceptance.suite.dist.yml +++ b/tests/Codeception/acceptance.suite.dist.yml @@ -17,7 +17,7 @@ modules: window_size: 1920x1080 capabilities: 'goog:chromeOptions': - args: ["headless", whitelisted-ips", "disable-gpu", "no-sandbox", "window-size=1920x1080", "--disable-dev-shm-usage"] + args: ["headless", "whitelisted-ips", "disable-gpu", "no-sandbox", "window-size=1920x1080", "--disable-dev-shm-usage"] name: 'jane doe' # Name for the Administrator username: 'ci-admin' # UserName for the Administrator password: 'joomla-17082005' # Password for the Administrator From e54043ddd5203bdaad55d5e4889b9bf856915d5d Mon Sep 17 00:00:00 2001 From: Allon Moritz Date: Sun, 24 Oct 2021 12:07:05 +0200 Subject: [PATCH 48/57] Test all endpoints --- .../src/Controller/MediaController.php | 16 +- .../com_media/src/Model/MediumModel.php | 5 +- plugins/webservices/media/media.php | 2 + tests/Codeception/_support/Helper/Api.php | 12 + .../acceptance/01-install/InstallCest.php | 2 +- tests/Codeception/api.suite.dist.yml | 4 +- tests/Codeception/api/com_media/MediaCest.php | 295 +++++++++++++++++- 7 files changed, 320 insertions(+), 16 deletions(-) diff --git a/api/components/com_media/src/Controller/MediaController.php b/api/components/com_media/src/Controller/MediaController.php index cf7c66b2c16d1..b1f52df13026c 100644 --- a/api/components/com_media/src/Controller/MediaController.php +++ b/api/components/com_media/src/Controller/MediaController.php @@ -20,6 +20,7 @@ use Joomla\Component\Media\Administrator\Exception\InvalidPathException; use Joomla\Component\Media\Api\Helper\AdapterTrait; use Joomla\Component\Media\Api\Helper\MediaHelper; +use Joomla\Component\Media\Api\Model\MediumModel; use Joomla\String\Inflector; use Tobscure\JsonApi\Exception\InvalidParameterException; @@ -215,7 +216,8 @@ public function add(): void $missingParameters[] = 'path'; } - if (empty($content)) + // Content is only required when it is a file + if (empty($content) && strpos($path, '.')) { $missingParameters[] = 'content'; } @@ -285,7 +287,7 @@ public function edit(): void // For renaming/moving files, we need the path to the existing file or folder. $this->modelState->set('old_path', $this->input->get('path', '', 'STRING')); // Check if an existing file may be overwritten. Defaults to true. - $this->modelState->set('override', $this->input->json->get('override', false)); + $this->modelState->set('override', $this->input->json->get('override', true)); $recordId = $this->save(); @@ -312,17 +314,19 @@ protected function allowEdit($data = array(), $key = 'id'): bool /** * Method to create or modify a file or folder. * - * @param int $recordKey The primary key of the item (if exists) + * @param integer $recordKey The primary key of the item (if exists) * - * @return int The record ID on success, false on failure + * @return string The path * * @since __DEPLOY_VERSION__ */ - protected function save($recordKey = null): int + protected function save($recordKey = null) { // Explicitly get the single item model name. $modelName = $this->input->get('model', Inflector::singularize($this->contentType)); - $model = $this->getModel($modelName, '', ['ignore_request' => true, 'state' => $this->modelState]); + + /** @var MediumModel $model */ + $model = $this->getModel($modelName, '', ['ignore_request' => true, 'state' => $this->modelState]); $json = $this->input->json; diff --git a/api/components/com_media/src/Model/MediumModel.php b/api/components/com_media/src/Model/MediumModel.php index f77ee934e84af..e12e2cc071bc9 100644 --- a/api/components/com_media/src/Model/MediumModel.php +++ b/api/components/com_media/src/Model/MediumModel.php @@ -84,12 +84,13 @@ public function getItem() * * @param string $path The primary key of the item (if exists) * - * @return int The record ID on success, false on failure + * @return string The path * * @since __DEPLOY_VERSION__ + * * @throws Save */ - public function save($path = null): int + public function save($path = null): string { $path = $this->getState('path', ''); $oldPath = $this->getState('old_path', ''); diff --git a/plugins/webservices/media/media.php b/plugins/webservices/media/media.php index b1ebf1605453a..cde9ffe1c45e3 100644 --- a/plugins/webservices/media/media.php +++ b/plugins/webservices/media/media.php @@ -97,6 +97,8 @@ private function createMediaCRUDRoutes(&$router, $baseName, $controller, $defaul $routes = [ new Route(['GET'], $baseName, $controller . '.displayList', [], $getDefaults), + // When the path ends with a backslash, then list the items + new Route(['GET'], $baseName . '/:path/', $controller . '.displayList', ['path' => '.*\/'], $getDefaults), new Route(['GET'], $baseName . '/:path', $controller . '.displayItem', ['path' => '.*'], $getDefaults), new Route(['POST'], $baseName, $controller . '.add', [], $defaults), new Route(['PATCH'], $baseName . '/:path', $controller . '.edit', ['path' => '.*'], $defaults), diff --git a/tests/Codeception/_support/Helper/Api.php b/tests/Codeception/_support/Helper/Api.php index c1cfa3a17cdb0..f035f547a149e 100644 --- a/tests/Codeception/_support/Helper/Api.php +++ b/tests/Codeception/_support/Helper/Api.php @@ -47,4 +47,16 @@ public function getBearerToken(): string return 'c2hhMjU2OjM6ZTJmMjJlYTNlNTU0NmM1MDJhYTIzYzMwN2MxYzAwZTQ5NzJhMWRmOTUyNjY5MTk2YjE5ODJmZWMwZTcxNzgwMQ=='; } + + /** + * Creates a user for API authentication and returns a bearer token. + * + * @return string The token + * + * @since __DEPLOY_VERSION__ + */ + public function getConfig($name): string + { + return $this->getModule('Helper\Api')->_getConfig()[$name]; + } } diff --git a/tests/Codeception/acceptance/01-install/InstallCest.php b/tests/Codeception/acceptance/01-install/InstallCest.php index d7832b7042d47..2e3ef6b324963 100644 --- a/tests/Codeception/acceptance/01-install/InstallCest.php +++ b/tests/Codeception/acceptance/01-install/InstallCest.php @@ -26,7 +26,7 @@ class InstallCest public function installJoomla(AcceptanceTester $I) { $I->am('Administrator'); - $I->installJoomlaRemovingInstallationFolder(); + $I->installJoomla(); } /** diff --git a/tests/Codeception/api.suite.dist.yml b/tests/Codeception/api.suite.dist.yml index 12a74716bb1eb..a9dce952b734f 100644 --- a/tests/Codeception/api.suite.dist.yml +++ b/tests/Codeception/api.suite.dist.yml @@ -2,7 +2,7 @@ actor: ApiTester modules: enabled: - Helper\JoomlaDb - - \Helper\Api + - Helper\Api - REST: url: http://localhost/test-install/api/index.php/v1 depends: PhpBrowser @@ -13,3 +13,5 @@ modules: user: 'root' password: 'joomla_ut' prefix: 'jos_' + Helper\Api: + cmsPath: '/tests/www/test-install' diff --git a/tests/Codeception/api/com_media/MediaCest.php b/tests/Codeception/api/com_media/MediaCest.php index 8bf2b1fcb6671..991a67e91f585 100644 --- a/tests/Codeception/api/com_media/MediaCest.php +++ b/tests/Codeception/api/com_media/MediaCest.php @@ -7,6 +7,7 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ +use Codeception\Util\FileSystem; use Codeception\Util\HttpCode; /** @@ -19,11 +20,55 @@ class MediaCest { /** - * Test the media adapter endpoint of com_media from the API. + * The name of the test directory, which gets deleted after each test. + * + * @var string + * + * @since __DEPLOY_VERSION__ + */ + private $testDirectory = 'test-dir'; + + /** + * Runs before every test. + * + * @param mixed ApiTester $I Api tester + * + * @since __DEPLOY_VERSION__ + * + * @throws Exception + */ + public function _before(ApiTester $I) + { + if (file_exists($this->getImagesDirectory($I))) + { + FileSystem::deleteDir($this->getImagesDirectory($I)); + } + + // Create the test directory + mkdir($this->getImagesDirectory($I)); + } + + /** + * Runs after every test. + * + * @param mixed ApiTester $I Api tester + * + * @since 4.0.0 + * + * @throws Exception + */ + public function _after(ApiTester $I) + { + // Delete the test directory + FileSystem::deleteDir($this->getImagesDirectory($I)); + } + + /** + * Test the GET media adapter endpoint of com_media from the API. * * @param mixed ApiTester $I Api tester * - * @return void + * @return void * * @since __DEPLOY_VERSION__ */ @@ -32,16 +77,17 @@ public function testGetAdapters(ApiTester $I) $I->amBearerAuthenticated($I->getBearerToken()); $I->haveHttpHeader('Accept', 'application/vnd.api+json'); $I->sendGET('/media/adapters'); + $I->seeResponseCodeIs(HttpCode::OK); $I->seeResponseContainsJson(['provider_id' => 'local', 'name' => 'images']); } /** - * Test the media adapter endpoint of com_media from the API. + * Test the GET media adapter endpoint for a single adapter of com_media from the API. * * @param mixed ApiTester $I Api tester * - * @return void + * @return void * * @since __DEPLOY_VERSION__ */ @@ -50,16 +96,17 @@ public function testGetAdapter(ApiTester $I) $I->amBearerAuthenticated($I->getBearerToken()); $I->haveHttpHeader('Accept', 'application/vnd.api+json'); $I->sendGET('/media/adapters/local-images'); + $I->seeResponseCodeIs(HttpCode::OK); $I->seeResponseContainsJson(['provider_id' => 'local', 'name' => 'images']); } /** - * Test the media files endpoint of com_media from the API. + * Test the GET media files endpoint of com_media from the API. * * @param mixed ApiTester $I Api tester * - * @return void + * @return void * * @since __DEPLOY_VERSION__ */ @@ -68,6 +115,242 @@ public function testGetFiles(ApiTester $I) $I->amBearerAuthenticated($I->getBearerToken()); $I->haveHttpHeader('Accept', 'application/vnd.api+json'); $I->sendGET('/media/files'); + $I->seeResponseCodeIs(HttpCode::OK); + $I->seeResponseContainsJson(['data' => ['attributes' => ['type' => 'dir', 'name' => 'banners']]]); + $I->seeResponseContainsJson(['data' => ['attributes' => ['type' => 'file', 'name' => 'joomla_black.png']]]); + } + + /** + * Test the GET media files endpoint of com_media from the API. + * + * @param mixed ApiTester $I Api tester + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public function testGetFilesInSubfolder(ApiTester $I) + { + $I->amBearerAuthenticated($I->getBearerToken()); + $I->haveHttpHeader('Accept', 'application/vnd.api+json'); + $I->sendGET('/media/files/sampledata/cassiopeia/'); + + $I->seeResponseCodeIs(HttpCode::OK); + $I->seeResponseContainsJson(['data' => ['attributes' => ['type' => 'file', 'name' => 'nasa1-1200.jpg']]]); + } + + /** + * Test the GET media files endpoint of com_media from the API. + * + * @param mixed ApiTester $I Api tester + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public function testGetFilesWithAdapter(ApiTester $I) + { + $I->amBearerAuthenticated($I->getBearerToken()); + $I->haveHttpHeader('Accept', 'application/vnd.api+json'); + $I->sendGET('/media/files/local-images:/sampledata/cassiopeia/'); + + $I->seeResponseCodeIs(HttpCode::OK); + $I->seeResponseContainsJson(['data' => ['attributes' => ['type' => 'file', 'name' => 'nasa1-1200.jpg']]]); + } + + /** + * Test the GET media files endpoint for a single file of com_media from the API. + * + * @param mixed ApiTester $I Api tester + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public function testGetFile(ApiTester $I) + { + $I->amBearerAuthenticated($I->getBearerToken()); + $I->haveHttpHeader('Accept', 'application/vnd.api+json'); + $I->sendGET('/media/files/joomla_black.png'); + + $I->seeResponseCodeIs(HttpCode::OK); + $I->seeResponseContainsJson(['data' => ['attributes' => ['type' => 'file', 'name' => 'joomla_black.png']]]); + } + + /** + * Test the GET media files endpoint for a single file of com_media from the API. + * + * @param mixed ApiTester $I Api tester + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public function testGetFolder(ApiTester $I) + { + $I->amBearerAuthenticated($I->getBearerToken()); + $I->haveHttpHeader('Accept', 'application/vnd.api+json'); + $I->sendGET('/media/files/sampledata/cassiopeia'); + + $I->seeResponseCodeIs(HttpCode::OK); + $I->seeResponseContainsJson(['data' => ['attributes' => ['type' => 'dir', 'name' => 'cassiopeia']]]); + } + + /** + * Test the POST media files endpoint of com_media from the API. + * + * @param mixed ApiTester $I Api tester + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public function testCreateFile(ApiTester $I) + { + $I->amBearerAuthenticated($I->getBearerToken()); + $I->haveHttpHeader('Content-Type', 'application/json'); + $I->haveHttpHeader('Accept', 'application/vnd.api+json'); + $I->sendPost( + '/media/files', + [ + 'path' => $this->testDirectory . '/test.jpg', + 'content' => base64_encode(file_get_contents(codecept_data_dir() . '/com_media/test-image-1.jpg')) + ] + ); + + $I->seeResponseCodeIs(HttpCode::OK); + $I->seeResponseContainsJson(['data' => ['attributes' => ['type' => 'file', 'name' => 'test.jpg']]]); + } + + /** + * Test the POST media files endpoint of com_media from the API. + * + * @param mixed ApiTester $I Api tester + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public function testCreateFolder(ApiTester $I) + { + $I->amBearerAuthenticated($I->getBearerToken()); + $I->haveHttpHeader('Content-Type', 'application/json'); + $I->haveHttpHeader('Accept', 'application/vnd.api+json'); + $I->sendPost( + '/media/files', + ['path' => $this->testDirectory . '/test-from-create'] + ); + + $I->seeResponseCodeIs(HttpCode::OK); + $I->seeResponseContainsJson(['data' => ['attributes' => ['type' => 'dir', 'name' => 'test-from-create']]]); + } + + /** + * Test the PATCH media files endpoint of com_media from the API. + * + * @param mixed ApiTester $I Api tester + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public function testUpdateFile(ApiTester $I) + { + file_put_contents($this->getImagesDirectory($I) . '/override.jpg', '1'); + + $I->amBearerAuthenticated($I->getBearerToken()); + $I->haveHttpHeader('Content-Type', 'application/json'); + $I->haveHttpHeader('Accept', 'application/vnd.api+json'); + $I->sendPatch( + '/media/files/' . $this->testDirectory . '/override.jpg', + [ + 'path' => $this->testDirectory . '/override.jpg', + 'content' => base64_encode(file_get_contents(codecept_data_dir() . '/com_media/test-image-1.jpg')) + ] + ); + + $I->seeResponseCodeIs(HttpCode::OK); + $I->seeResponseContainsJson(['data' => ['attributes' => ['type' => 'file', 'name' => 'override.jpg']]]); + $I->dontSeeResponseContainsJson(['data' => ['attributes' => ['content' => '1']]]); + } + + /** + * Test the PATCH media files endpoint of com_media from the API. + * + * @param mixed ApiTester $I Api tester + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public function testUpdateFolder(ApiTester $I) + { + mkdir($this->getImagesDirectory($I) . '/override'); + + $I->amBearerAuthenticated($I->getBearerToken()); + $I->haveHttpHeader('Content-Type', 'application/json'); + $I->haveHttpHeader('Accept', 'application/vnd.api+json'); + $I->sendPatch( + '/media/files/' . $this->testDirectory . '/override', + ['path' => $this->testDirectory . '/override-new'] + ); + + $I->seeResponseCodeIs(HttpCode::OK); + $I->seeResponseContainsJson(['data' => ['attributes' => ['type' => 'dir', 'name' => 'override-new']]]); + } + + /** + * Test the DELETE media files endpoint of com_media from the API. + * + * @param mixed ApiTester $I Api tester + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public function testDeleteFile(ApiTester $I) + { + touch($this->getImagesDirectory($I) . '/todelete.jpg'); + + $I->amBearerAuthenticated($I->getBearerToken()); + $I->haveHttpHeader('Accept', 'application/vnd.api+json'); + $I->sendDelete('/media/files/' . $this->testDirectory . '/todelete.jpg'); + + $I->seeResponseCodeIs(HttpCode::NO_CONTENT); + } + + /** + * Test the DELETE media files endpoint of com_media from the API. + * + * @param mixed ApiTester $I Api tester + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public function testDeleteFolder(ApiTester $I) + { + mkdir($this->getImagesDirectory($I) . '/todelete'); + + $I->amBearerAuthenticated($I->getBearerToken()); + $I->haveHttpHeader('Accept', 'application/vnd.api+json'); + $I->sendDelete('/media/files/' . $this->testDirectory . '/todelete'); + + $I->seeResponseCodeIs(HttpCode::NO_CONTENT); + } + + /** + * Returns the absolute tmp image folder path to work on. + * + * @param mixed ApiTester $I Api tester + * + * @return string The absolute folder path + * + * @since __DEPLOY_VERSION__ + */ + private function getImagesDirectory(ApiTester $I): string + { + return $I->getConfig('cmsPath') . '/images/' . $this->testDirectory; } } From 26cd1d8dd3be37221b455493e26da9864309af98 Mon Sep 17 00:00:00 2001 From: Allon Moritz Date: Sun, 24 Oct 2021 12:50:56 +0200 Subject: [PATCH 49/57] Update api/components/com_media/src/Model/MediaModel.php Co-authored-by: Phil E. Taylor --- api/components/com_media/src/Model/MediaModel.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/components/com_media/src/Model/MediaModel.php b/api/components/com_media/src/Model/MediaModel.php index bc80700962378..f4f3b0f7d4ab0 100644 --- a/api/components/com_media/src/Model/MediaModel.php +++ b/api/components/com_media/src/Model/MediaModel.php @@ -82,7 +82,7 @@ public function getItems(): array ); } - /* + /** * A hacky way to enable the standard jsonapiView::displayList() to create a Pagination object. * Because com_media's ApiModel does not support pagination as we know from regular ListModel * derived models, we always return all retrieved items. From 245e36aa7e121387775c6f9ff65a73b584073794 Mon Sep 17 00:00:00 2001 From: Allon Moritz Date: Sun, 24 Oct 2021 12:52:02 +0200 Subject: [PATCH 50/57] Hardening --- api/components/com_media/src/Controller/MediaController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/components/com_media/src/Controller/MediaController.php b/api/components/com_media/src/Controller/MediaController.php index b1f52df13026c..7fd706b3cc682 100644 --- a/api/components/com_media/src/Controller/MediaController.php +++ b/api/components/com_media/src/Controller/MediaController.php @@ -217,7 +217,7 @@ public function add(): void } // Content is only required when it is a file - if (empty($content) && strpos($path, '.')) + if (empty($content) && strpos($path, '.') !== false) { $missingParameters[] = 'content'; } From 2c9ee7a377076ec18ecfe0ea3c36e93eaba2be15 Mon Sep 17 00:00:00 2001 From: Allon Moritz Date: Sun, 24 Oct 2021 13:16:05 +0200 Subject: [PATCH 51/57] Tests fix for drone --- tests/Codeception/_support/Helper/Api.php | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/tests/Codeception/_support/Helper/Api.php b/tests/Codeception/_support/Helper/Api.php index f035f547a149e..f8f49e0b26f53 100644 --- a/tests/Codeception/_support/Helper/Api.php +++ b/tests/Codeception/_support/Helper/Api.php @@ -37,7 +37,19 @@ public function getBearerToken(): string if (!$db->grabFromDatabase('users', 'id', ['id' => $desiredUserId])) { - $db->haveInDatabase('users', ['id' => $desiredUserId, 'name' => 'API', 'email' => 'api@example.com', 'username' => 'api', 'password' => '123', 'block' => 0], []); + $db->haveInDatabase( + 'users', + [ + 'id' => $desiredUserId, + 'name' => 'API', + 'email' => 'api@example.com', + 'username' => 'api', + 'password' => '123', + 'block' => 0, + 'registerDate' => '2000-01-01' + ], + [] + ); $db->haveInDatabase('user_usergroup_map', ['user_id' => $desiredUserId, 'group_id' => 8]); $enabledData = ['user_id' => $desiredUserId, 'profile_key' => 'joomlatoken.enabled', 'profile_value' => 1]; $tokenData = ['user_id' => $desiredUserId, 'profile_key' => 'joomlatoken.token', 'profile_value' => 'dOi2m1NRrnBHlhaWK/WWxh3B5tqq1INbdf4DhUmYTI4=']; From be5e17c7488eaceddca5133feb42d31e63c1b432 Mon Sep 17 00:00:00 2001 From: Allon Moritz Date: Sun, 24 Oct 2021 13:40:41 +0200 Subject: [PATCH 52/57] Update Api.php --- tests/Codeception/_support/Helper/Api.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/Codeception/_support/Helper/Api.php b/tests/Codeception/_support/Helper/Api.php index f8f49e0b26f53..1088cb0e398a5 100644 --- a/tests/Codeception/_support/Helper/Api.php +++ b/tests/Codeception/_support/Helper/Api.php @@ -46,7 +46,8 @@ public function getBearerToken(): string 'username' => 'api', 'password' => '123', 'block' => 0, - 'registerDate' => '2000-01-01' + 'registerDate' => '2000-01-01', + 'params' => '{}' ], [] ); From cdd607ede6aed5ec6759b6d3711f69821bfdefa0 Mon Sep 17 00:00:00 2001 From: Allon Moritz Date: Fri, 5 Nov 2021 08:17:18 +0100 Subject: [PATCH 53/57] Create directory in test with correct permissions --- tests/Codeception/api.suite.dist.yml | 1 + tests/Codeception/api/com_media/MediaCest.php | 12 ++++++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/tests/Codeception/api.suite.dist.yml b/tests/Codeception/api.suite.dist.yml index a9dce952b734f..bba987d8d822f 100644 --- a/tests/Codeception/api.suite.dist.yml +++ b/tests/Codeception/api.suite.dist.yml @@ -15,3 +15,4 @@ modules: prefix: 'jos_' Helper\Api: cmsPath: '/tests/www/test-install' + localUser: 'www-data' diff --git a/tests/Codeception/api/com_media/MediaCest.php b/tests/Codeception/api/com_media/MediaCest.php index 991a67e91f585..07b88764bbb18 100644 --- a/tests/Codeception/api/com_media/MediaCest.php +++ b/tests/Codeception/api/com_media/MediaCest.php @@ -44,8 +44,16 @@ public function _before(ApiTester $I) FileSystem::deleteDir($this->getImagesDirectory($I)); } - // Create the test directory - mkdir($this->getImagesDirectory($I)); + // Copied from \Step\Acceptance\Administrator\Media:createDirectory() + $oldUmask = @umask(0); + @mkdir($this->getImagesDirectory($I), 0755, true); + + if (!empty($user = $I->getConfig('localUser'))) + { + @chown($this->getImagesDirectory($I), $user); + } + + @umask($oldUmask); } /** From 348abb6aa7a5d24250e1794c43226478ce6c5fec Mon Sep 17 00:00:00 2001 From: Allon Moritz Date: Fri, 5 Nov 2021 09:50:45 +0100 Subject: [PATCH 54/57] Add more tests and remove static helper --- .../src/Controller/MediaController.php | 4 - .../com_media/src/Helper/AdapterTrait.php | 96 +++++++++++++++++-- .../com_media/src/Helper/MediaHelper.php | 73 -------------- .../com_media/src/Model/MediaModel.php | 6 +- .../com_media/src/Model/MediumModel.php | 24 ++--- tests/Codeception/_support/Helper/Api.php | 9 +- tests/Codeception/api.suite.dist.yml | 1 + tests/Codeception/api/com_media/MediaCest.php | 41 ++++++++ 8 files changed, 147 insertions(+), 107 deletions(-) delete mode 100644 api/components/com_media/src/Helper/MediaHelper.php diff --git a/api/components/com_media/src/Controller/MediaController.php b/api/components/com_media/src/Controller/MediaController.php index 7fd706b3cc682..d4b1353272355 100644 --- a/api/components/com_media/src/Controller/MediaController.php +++ b/api/components/com_media/src/Controller/MediaController.php @@ -19,7 +19,6 @@ use Joomla\Component\Media\Administrator\Exception\FileExistsException; use Joomla\Component\Media\Administrator\Exception\InvalidPathException; use Joomla\Component\Media\Api\Helper\AdapterTrait; -use Joomla\Component\Media\Api\Helper\MediaHelper; use Joomla\Component\Media\Api\Model\MediumModel; use Joomla\String\Inflector; use Tobscure\JsonApi\Exception\InvalidParameterException; @@ -330,9 +329,6 @@ protected function save($recordKey = null) $json = $this->input->json; - // Split destination path into adapter name and file path. - ['adapter' => $adapter, 'path' => $path] = MediaHelper::adapterNameAndPath($this->input->get('path', '', 'STRING')); - // Decode content, if any if ($content = base64_decode($json->get('content', '', 'raw'))) { diff --git a/api/components/com_media/src/Helper/AdapterTrait.php b/api/components/com_media/src/Helper/AdapterTrait.php index e323a3d0cb3ab..54155908cef06 100644 --- a/api/components/com_media/src/Helper/AdapterTrait.php +++ b/api/components/com_media/src/Helper/AdapterTrait.php @@ -11,6 +11,7 @@ \defined('_JEXEC') or die; +use Joomla\CMS\Component\ComponentHelper; use Joomla\CMS\Factory; use Joomla\CMS\Plugin\PluginHelper; use Joomla\Component\Media\Administrator\Adapter\AdapterInterface; @@ -28,18 +29,64 @@ trait AdapterTrait /** * Holds the available media file adapters. * - * @var ProviderManager + * @var ProviderManager * * @since __DEPLOY_VERSION__ */ private $providerManager = null; + /** + * The default adapter name. + * + * @var string + * + * @since __DEPLOY_VERSION__ + */ + private $defaultAdapterName = null; + + /** + * Returns an array with the adapter name as key and the path of the file. + * + * @return array + * + * @throws \Exception + * + * @since __DEPLOY_VERSION__ + */ + private 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('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 a provider for the given id. * - * @return ProviderInterface + * @return ProviderInterface * - * @throws \Exception + * @throws \Exception * * @since __DEPLOY_VERSION__ */ @@ -51,9 +98,9 @@ private function getProvider(String $id): ProviderInterface /** * Return an adapter for the given name. * - * @return AdapterInterface + * @return AdapterInterface * - * @throws \Exception + * @throws \Exception * * @since __DEPLOY_VERSION__ */ @@ -62,10 +109,45 @@ private function getAdapter(String $name): AdapterInterface return $this->getProviderManager()->getAdapter($name); } + /** + * Returns the default adapter name. + * + * @return string|null + * + * @throws \Exception + * + * @since __DEPLOY_VERSION__ + */ + private 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; + } + /** * Return a provider manager. * - * @return ProviderManager + * @return ProviderManager * * @since __DEPLOY_VERSION__ */ @@ -73,7 +155,7 @@ private function getProviderManager(): ProviderManager { if (!$this->providerManager) { - $this->providerManager = new ProviderManager(); + $this->providerManager = new ProviderManager; // Fire the event to get the results $eventParameters = ['context' => 'AdapterManager', 'providerManager' => $this->providerManager]; diff --git a/api/components/com_media/src/Helper/MediaHelper.php b/api/components/com_media/src/Helper/MediaHelper.php deleted file mode 100644 index 4cfc7c16c8da2..0000000000000 --- a/api/components/com_media/src/Helper/MediaHelper.php +++ /dev/null @@ -1,73 +0,0 @@ - - * @license GNU General Public License version 2 or later; see LICENSE.txt - */ - -namespace Joomla\Component\Media\Api\Helper; - -\defined('_JEXEC') or die; - -use Joomla\CMS\Component\ComponentHelper; - -/** - * Helper methods for media web service. - * - * @since __DEPLOY_VERSION__ - */ -class MediaHelper -{ - /** - * Split a given path in adapter prefix and file path. - * - * @param string $path The path to split. - * - * @return array An array with elements 'adapter' and 'path'. - * - * @since __DEPLOY_VERSION__ - */ - public static function adapterNameAndPath(String $path) - { - $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 we have less than 2 parts, we return a default adapter name. - $result['adapter'] = self::defaultAdapterName(); - - // 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 The adapter name - * - * @since __DEPLOY_VERSION__ - */ - private static function defaultAdapterName(): string - { - static $comMediaParams; - - if (!$comMediaParams) - { - $comMediaParams = ComponentHelper::getParams('com_media'); - } - - return 'local-' . $comMediaParams->get('file_path', 'images'); - } -} diff --git a/api/components/com_media/src/Model/MediaModel.php b/api/components/com_media/src/Model/MediaModel.php index f4f3b0f7d4ab0..572ec19e3e16f 100644 --- a/api/components/com_media/src/Model/MediaModel.php +++ b/api/components/com_media/src/Model/MediaModel.php @@ -18,7 +18,7 @@ use Joomla\CMS\Pagination\Pagination; use Joomla\Component\Media\Administrator\Exception\FileNotFoundException; use Joomla\Component\Media\Administrator\Model\ApiModel; -use Joomla\Component\Media\Api\Helper\MediaHelper; +use Joomla\Component\Media\Api\Helper\AdapterTrait; /** * Media web service model supporting lists of media items. @@ -27,6 +27,8 @@ */ class MediaModel extends BaseModel implements ListModelInterface { + use AdapterTrait; + /** * Instance of com_media's ApiModel * @@ -69,7 +71,7 @@ public function getItems(): array 'content' => $this->getState('content', false), ]; - ['adapter' => $adapterName, 'path' => $path] = MediaHelper::adapterNameAndPath($this->getState('path', '')); + ['adapter' => $adapterName, 'path' => $path] = $this->resolveAdapterAndPath($this->getState('path', '')); try { $files = $this->mediaApiModel->getFiles($adapterName, $path, $options); diff --git a/api/components/com_media/src/Model/MediumModel.php b/api/components/com_media/src/Model/MediumModel.php index e12e2cc071bc9..9768526274bf9 100644 --- a/api/components/com_media/src/Model/MediumModel.php +++ b/api/components/com_media/src/Model/MediumModel.php @@ -19,7 +19,7 @@ use Joomla\Component\Media\Administrator\Exception\FileNotFoundException; use Joomla\Component\Media\Administrator\Exception\InvalidPathException; use Joomla\Component\Media\Administrator\Model\ApiModel; -use Joomla\Component\Media\Api\Helper\MediaHelper; +use Joomla\Component\Media\Api\Helper\AdapterTrait; /** * Media web service model supporting a single media item. @@ -28,6 +28,8 @@ */ class MediumModel extends BaseModel { + use AdapterTrait; + /** * Instance of com_media's ApiModel * @@ -60,11 +62,7 @@ public function getItem() 'content' => $this->getState('content', false), ]; - [ - 'adapter' => $adapterName, - 'path' => $path, - ] - = MediaHelper::adapterNameAndPath($this->getState('path', '')); + ['adapter' => $adapterName, 'path' => $path] = $this->resolveAdapterAndPath($this->getState('path', '')); try { @@ -97,11 +95,7 @@ public function save($path = null): string $content = $this->getState('content', null); $override = $this->getState('override', false); - [ - 'adapter' => $adapterName, - 'path' => $path, - ] - = MediaHelper::adapterNameAndPath($path); + ['adapter' => $adapterName, 'path' => $path] = $this->resolveAdapterAndPath($path); $resultPath = ''; @@ -260,13 +254,7 @@ public function save($path = null): string */ public function delete(): void { - $path = $this->getState('path', ''); - - [ - 'adapter' => $adapterName, - 'path' => $path, - ] - = MediaHelper::adapterNameAndPath($path); + ['adapter' => $adapterName, 'path' => $path] = $this->resolveAdapterAndPath($this->getState('path', '')); try { diff --git a/tests/Codeception/_support/Helper/Api.php b/tests/Codeception/_support/Helper/Api.php index 1088cb0e398a5..2cccd78f7dddf 100644 --- a/tests/Codeception/_support/Helper/Api.php +++ b/tests/Codeception/_support/Helper/Api.php @@ -64,12 +64,15 @@ public function getBearerToken(): string /** * Creates a user for API authentication and returns a bearer token. * - * @return string The token + * @param string $name The name of the config key + * @param string $module The module + * + * @return string The config key * * @since __DEPLOY_VERSION__ */ - public function getConfig($name): string + public function getConfig($name, $module = 'Helper\Api'): string { - return $this->getModule('Helper\Api')->_getConfig()[$name]; + return $this->getModule($module)->_getConfig()[$name]; } } diff --git a/tests/Codeception/api.suite.dist.yml b/tests/Codeception/api.suite.dist.yml index bba987d8d822f..a8f50914482e4 100644 --- a/tests/Codeception/api.suite.dist.yml +++ b/tests/Codeception/api.suite.dist.yml @@ -14,5 +14,6 @@ modules: password: 'joomla_ut' prefix: 'jos_' Helper\Api: + url: 'http://localhost/test-install' cmsPath: '/tests/www/test-install' localUser: 'www-data' diff --git a/tests/Codeception/api/com_media/MediaCest.php b/tests/Codeception/api/com_media/MediaCest.php index 07b88764bbb18..b6cd494bbfabc 100644 --- a/tests/Codeception/api/com_media/MediaCest.php +++ b/tests/Codeception/api/com_media/MediaCest.php @@ -167,6 +167,27 @@ public function testGetFilesWithAdapter(ApiTester $I) $I->seeResponseContainsJson(['data' => ['attributes' => ['type' => 'file', 'name' => 'nasa1-1200.jpg']]]); } + /** + * Test the GET media files endpoint of com_media from the API. + * + * @param mixed ApiTester $I Api tester + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public function testSearchFiles(ApiTester $I) + { + $I->amBearerAuthenticated($I->getBearerToken()); + $I->haveHttpHeader('Accept', 'application/vnd.api+json'); + $I->sendGET('/media/files?filter[search]=joomla'); + + $I->seeResponseCodeIs(HttpCode::OK); + $I->seeResponseContainsJson(['data' => ['attributes' => ['type' => 'file', 'name' => 'joomla_black.png']]]); + $I->dontSeeResponseContainsJson(['data' => ['attributes' => ['type' => 'dir', 'name' => 'powered_by.png']]]); + $I->dontSeeResponseContainsJson(['data' => ['attributes' => ['type' => 'dir', 'name' => 'banners']]]); + } + /** * Test the GET media files endpoint for a single file of com_media from the API. * @@ -184,6 +205,26 @@ public function testGetFile(ApiTester $I) $I->seeResponseCodeIs(HttpCode::OK); $I->seeResponseContainsJson(['data' => ['attributes' => ['type' => 'file', 'name' => 'joomla_black.png']]]); + $I->dontSeeResponseContainsJson(['data' => ['attributes' => ['url' => $I->getConfig('url') . '/images/joomla_black.png']]]); + } + + /** + * Test the GET media files endpoint for a single file of com_media from the API. + * + * @param mixed ApiTester $I Api tester + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public function testGetFileWithUrl(ApiTester $I) + { + $I->amBearerAuthenticated($I->getBearerToken()); + $I->haveHttpHeader('Accept', 'application/vnd.api+json'); + $I->sendGET('/media/files/joomla_black.png?url=1'); + + $I->seeResponseCodeIs(HttpCode::OK); + $I->seeResponseContainsJson(['data' => ['attributes' => ['url' => $I->getConfig('url') . '/images/joomla_black.png']]]); } /** From 2f867ba3d15990037486081040d63bac6e81fdbc Mon Sep 17 00:00:00 2001 From: Allon Moritz Date: Mon, 15 Nov 2021 08:08:20 +0100 Subject: [PATCH 55/57] Update tests/Codeception/api/com_media/MediaCest.php Co-authored-by: Phil E. Taylor --- tests/Codeception/api/com_media/MediaCest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Codeception/api/com_media/MediaCest.php b/tests/Codeception/api/com_media/MediaCest.php index b6cd494bbfabc..e6f973cba5d1b 100644 --- a/tests/Codeception/api/com_media/MediaCest.php +++ b/tests/Codeception/api/com_media/MediaCest.php @@ -61,7 +61,7 @@ public function _before(ApiTester $I) * * @param mixed ApiTester $I Api tester * - * @since 4.0.0 + * @since __DEPLOY_VERSION__ * * @throws Exception */ From c2059a99d11e8b8d3dae40813b8f1a515b4b8957 Mon Sep 17 00:00:00 2001 From: Allon Moritz Date: Mon, 15 Nov 2021 08:08:28 +0100 Subject: [PATCH 56/57] Update tests/Codeception/_support/Helper/Api.php Co-authored-by: Phil E. Taylor --- tests/Codeception/_support/Helper/Api.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Codeception/_support/Helper/Api.php b/tests/Codeception/_support/Helper/Api.php index 2cccd78f7dddf..cf6fd685e4b3c 100644 --- a/tests/Codeception/_support/Helper/Api.php +++ b/tests/Codeception/_support/Helper/Api.php @@ -61,7 +61,7 @@ public function getBearerToken(): string return 'c2hhMjU2OjM6ZTJmMjJlYTNlNTU0NmM1MDJhYTIzYzMwN2MxYzAwZTQ5NzJhMWRmOTUyNjY5MTk2YjE5ODJmZWMwZTcxNzgwMQ=='; } - /** + /** * Creates a user for API authentication and returns a bearer token. * * @param string $name The name of the config key From d1bc84a25c7c4e79fdb5e2b5b695d16a86711dca Mon Sep 17 00:00:00 2001 From: Allon Moritz Date: Mon, 15 Nov 2021 08:11:38 +0100 Subject: [PATCH 57/57] cs --- .../com_media/src/Controller/MediaController.php | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/api/components/com_media/src/Controller/MediaController.php b/api/components/com_media/src/Controller/MediaController.php index d4b1353272355..379d275f99be7 100644 --- a/api/components/com_media/src/Controller/MediaController.php +++ b/api/components/com_media/src/Controller/MediaController.php @@ -49,19 +49,19 @@ class MediaController extends ApiController private static $listQueryModelStateMap = [ 'path' => [ 'name' => 'path', - 'type' => 'STRING' + 'type' => 'STRING', ], 'url' => [ 'name' => 'url', - 'type' => 'BOOLEAN' + 'type' => 'BOOLEAN', ], 'temp' => [ 'name' => 'temp', - 'type' => 'BOOLEAN' + 'type' => 'BOOLEAN', ], 'content' => [ 'name' => 'content', - 'type' => 'BOOLEAN' + 'type' => 'BOOLEAN', ], ]; @@ -74,19 +74,19 @@ class MediaController extends ApiController private static $itemQueryModelStateMap = [ 'path' => [ 'name' => 'path', - 'type' => 'STRING' + 'type' => 'STRING', ], 'url' => [ 'name' => 'url', - 'type' => 'BOOLEAN' + 'type' => 'BOOLEAN', ], 'temp' => [ 'name' => 'temp', - 'type' => 'BOOLEAN' + 'type' => 'BOOLEAN', ], 'content' => [ 'name' => 'content', - 'type' => 'BOOLEAN' + 'type' => 'BOOLEAN', ], ];