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..b2b25bba1114b --- /dev/null +++ b/administrator/language/en-GB/plg_webservices_media.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/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/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/Controller/MediaController.php b/api/components/com_media/src/Controller/MediaController.php new file mode 100644 index 0000000000000..379d275f99be7 --- /dev/null +++ b/api/components/com_media/src/Controller/MediaController.php @@ -0,0 +1,410 @@ + + * @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\Component\ComponentHelper; +use Joomla\CMS\Filter\InputFilter; +use Joomla\CMS\Language\Text; +use Joomla\CMS\MVC\Controller\ApiController; +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\Model\MediumModel; +use Joomla\String\Inflector; +use Tobscure\JsonApi\Exception\InvalidParameterException; + +/** + * Media web service controller. + * + * @since __DEPLOY_VERSION__ + */ +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')) + { + $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'; + } + + // Content is only required when it is a file + if (empty($content) && strpos($path, '.') !== false) + { + $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', 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 __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 string The path + * + * @since __DEPLOY_VERSION__ + */ + protected function save($recordKey = null) + { + // Explicitly get the single item model name. + $modelName = $this->input->get('model', Inflector::singularize($this->contentType)); + + /** @var MediumModel $model */ + $model = $this->getModel($modelName, '', ['ignore_request' => true, 'state' => $this->modelState]); + + $json = $this->input->json; + + // 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'); + } +} 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..54155908cef06 --- /dev/null +++ b/api/components/com_media/src/Helper/AdapterTrait.php @@ -0,0 +1,169 @@ + + * @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; +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\ProviderInterface; +use Joomla\Component\Media\Administrator\Provider\ProviderManager; + +/** + * Trait for classes that need adapters. + * + * @since __DEPLOY_VERSION__ + */ +trait AdapterTrait +{ + /** + * Holds the available media file adapters. + * + * @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 + * + * @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 + * + * @throws \Exception + * + * @since __DEPLOY_VERSION__ + */ + 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 + * + * @since __DEPLOY_VERSION__ + */ + private function getProviderManager(): ProviderManager + { + 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; + } +} 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..381306c9bb63f --- /dev/null +++ b/api/components/com_media/src/Model/AdapterModel.php @@ -0,0 +1,53 @@ + + * @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\Api\Helper\AdapterTrait; + +/** + * Media web service model supporting a single adapter item. + * + * @since __DEPLOY_VERSION__ + */ +class AdapterModel extends BaseModel +{ + use AdapterTrait; + + /** + * Method to get a single adapter. + * + * @return \stdClass The adapter. + * + * @since __DEPLOY_VERSION__ + */ + public function getItem(): \stdClass + { + list($provider, $account) = array_pad(explode('-', $this->getState('id'), 2), 2, null); + + if ($account === null) + { + throw new \Exception('Account was not set'); + } + + $provider = $this->getProvider($provider); + $adapter = $this->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..351b79ee9aba0 --- /dev/null +++ b/api/components/com_media/src/Model/AdaptersModel.php @@ -0,0 +1,104 @@ + + * @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\Api\Helper\AdapterTrait; + +/** + * Media web service model supporting lists of media adapters. + * + * @since __DEPLOY_VERSION__ + */ +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. + * + * @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 + { + $adapters = []; + foreach ($this->getProviderManager()->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/Model/MediaModel.php b/api/components/com_media/src/Model/MediaModel.php new file mode 100644 index 0000000000000..572ec19e3e16f --- /dev/null +++ b/api/components/com_media/src/Model/MediaModel.php @@ -0,0 +1,134 @@ + + * @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\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\AdapterTrait; + +/** + * Media web service model supporting lists of media items. + * + * @since __DEPLOY_VERSION__ + */ +class MediaModel extends BaseModel implements ListModelInterface +{ + use AdapterTrait; + + /** + * 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; + + 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 __DEPLOY_VERSION__ + */ + public function getItems(): array + { + // 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), + ]; + + ['adapter' => $adapterName, 'path' => $path] = $this->resolveAdapterAndPath($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 + * 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 __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 int 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 int 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/Model/MediumModel.php b/api/components/com_media/src/Model/MediumModel.php new file mode 100644 index 0000000000000..9768526274bf9 --- /dev/null +++ b/api/components/com_media/src/Model/MediumModel.php @@ -0,0 +1,271 @@ + + * @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\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; +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\AdapterTrait; + +/** + * Media web service model supporting a single media item. + * + * @since __DEPLOY_VERSION__ + */ +class MediumModel extends BaseModel +{ + use AdapterTrait; + + /** + * Instance of com_media's ApiModel + * + * @var ApiModel + * @since __DEPLOY_VERSION__ + */ + 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 __DEPLOY_VERSION__ + * @throws ResourceNotFound + */ + public function getItem() + { + $options = [ + 'path' => $this->getState('path', ''), + 'url' => $this->getState('url', false), + 'temp' => $this->getState('temp', false), + 'content' => $this->getState('content', false), + ]; + + ['adapter' => $adapterName, 'path' => $path] = $this->resolveAdapterAndPath($this->getState('path', '')); + + try + { + return $this->mediaApiModel->getFile($adapterName, $path, $options); + } + catch (FileNotFoundException $e) + { + throw new ResourceNotFound( + Text::sprintf('WEBSERVICE_COM_MEDIA_FILE_NOT_FOUND', $path), + 404 + ); + } + } + + /** + * Method to save a file or folder. + * + * @param string $path The primary key of the item (if exists) + * + * @return string The path + * + * @since __DEPLOY_VERSION__ + * + * @throws Save + */ + public function save($path = null): string + { + $path = $this->getState('path', ''); + $oldPath = $this->getState('old_path', ''); + $content = $this->getState('content', null); + $override = $this->getState('override', false); + + ['adapter' => $adapterName, 'path' => $path] = $this->resolveAdapterAndPath($path); + + $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 ($path && $oldPath) + { + 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 + // new file or folder. + if ($path && !$oldPath) + { + // 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); + + 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, + // 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); + + try + { + $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( + Text::sprintf( + 'WEBSERVICE_COM_MEDIA_BAD_FILE_TYPE', + $dirname . '/' . $basename + ), + 400 + ); + } + + $resultPath = $resultPath ?: $oldPath; + } + + // If we still have no result path, something fishy is going on. + if (!$resultPath) + { + throw new Save( + Text::_( + 'WEBSERVICE_COM_MEDIA_UNSUPPORTED_PARAMETER_COMBINATION' + ), + 400 + ); + } + + return $resultPath; + } + + /** + * Method to delete an existing file or folder. + * + * @return void + * + * @since __DEPLOY_VERSION__ + * @throws Save + */ + public function delete(): void + { + ['adapter' => $adapterName, 'path' => $path] = $this->resolveAdapterAndPath($this->getState('path', '')); + + try + { + $this->mediaApiModel->delete($adapterName, $path); + } + catch (FileNotFoundException $e) + { + throw new Save( + Text::sprintf('WEBSERVICE_COM_MEDIA_FILE_NOT_FOUND', $path), + 404 + ); + } + } +} 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/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..84ee67bf32ede --- /dev/null +++ b/api/components/com_media/src/View/Media/JsonapiView.php @@ -0,0 +1,95 @@ + + * @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; + +/** + * 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 = [ + 'type', + 'name', + 'path', + 'extension', + 'size', + 'mime_type', + 'width', + 'height', + 'create_date', + 'create_date_formatted', + 'modified_date', + 'modified_date_formatted', + 'thumb_path', + 'adapter', + 'content', + 'url', + 'tempUrl', + ]; + + /** + * The fields to render items in the documents + * + * @var array + * @since __DEPLOY_VERSION__ + */ + 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', + 'url', + 'tempUrl', + ]; + + /** + * Prepare item before render. + * + * @param object $item The model item + * + * @return object + * + * @since __DEPLOY_VERSION__ + */ + protected function prepareItem($item) + { + // Media resources have no id. + $item->id = '0'; + + return $item; + } +} diff --git a/api/language/en-GB/com_media.ini b/api/language/en-GB/com_media.ini new file mode 100644 index 0000000000000..3e36202c201d7 --- /dev/null +++ b/api/language/en-GB/com_media.ini @@ -0,0 +1,11 @@ +; 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 + +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" diff --git a/composer.json b/composer.json index 75a1266667df8..c760f66ef3f7c 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 ed849b302350d..df658ab10845b 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": "1da875478fc037b5b7b0c997043f2416", + "content-hash": "7a38a492e1140d3acdd45a4fb7f42486", "packages": [ { "name": "algo26-matthias/idna-convert", @@ -6725,6 +6725,636 @@ ], "time": "2021-10-06T17:43:30+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", 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), diff --git a/libraries/src/Error/JsonApi/SaveExceptionHandler.php b/libraries/src/Error/JsonApi/SaveExceptionHandler.php index fe76941e55e21..a8ae0a15dffef 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]); } diff --git a/plugins/webservices/media/media.php b/plugins/webservices/media/media.php new file mode 100644 index 0000000000000..cde9ffe1c45e3 --- /dev/null +++ b/plugins/webservices/media/media.php @@ -0,0 +1,110 @@ + + * @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 __DEPLOY_VERSION__ + */ +class PlgWebservicesMedia extends CMSPlugin +{ + /** + * Load the language file on instantiation. + * + * @var boolean + * @since __DEPLOY_VERSION__ + */ + protected $autoloadLanguage = true; + + /** + * Registers com_media's API's routes in the application. + * + * @param ApiRouter &$router The API Routing object + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public function onBeforeApiRoute(&$router): void + { + $this->createAdapterReadRoutes( + $router, + 'v1/media/adapters', + 'adapters', + ['component' => 'com_media'] + ); + $this->createMediaCRUDRoutes( + $router, + 'v1/media/files', + 'media', + ['component' => 'com_media'] + ); + } + + /** + * 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. + * @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 createMediaCRUDRoutes(&$router, $baseName, $controller, $defaults = [], $publicGets = false): void + { + $getDefaults = array_merge(['public' => $publicGets], $defaults); + + $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), + 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..95574782634dd --- /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 + __DEPLOY_VERSION__ + 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 + + diff --git a/tests/Codeception/_support/Helper/Api.php b/tests/Codeception/_support/Helper/Api.php index c0c4e97747a7e..cf6fd685e4b3c 100644 --- a/tests/Codeception/_support/Helper/Api.php +++ b/tests/Codeception/_support/Helper/Api.php @@ -21,4 +21,58 @@ */ 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, + 'registerDate' => '2000-01-01', + 'params' => '{}' + ], + [] + ); + $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=='; + } + + /** + * Creates a user for API authentication and returns a bearer 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, $module = 'Helper\Api'): string + { + return $this->getModule($module)->_getConfig()[$name]; + } } 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/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..a8f50914482e4 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,7 @@ modules: user: 'root' 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/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..3eb10f39fb6e5 100644 --- a/tests/Codeception/api/com_banners/BannerCest.php +++ b/tests/Codeception/api/com_banners/BannerCest.php @@ -29,27 +29,8 @@ class BannerCest */ 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..ce3f93e865d66 100644 --- a/tests/Codeception/api/com_contact/ContactCest.php +++ b/tests/Codeception/api/com_contact/ContactCest.php @@ -29,27 +29,8 @@ class ContactCest */ 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..9a75b6cc35e79 100644 --- a/tests/Codeception/api/com_content/ContentCest.php +++ b/tests/Codeception/api/com_content/ContentCest.php @@ -29,27 +29,8 @@ class ContentCest */ 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..e6f973cba5d1b --- /dev/null +++ b/tests/Codeception/api/com_media/MediaCest.php @@ -0,0 +1,405 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +use Codeception\Util\FileSystem; +use Codeception\Util\HttpCode; + +/** + * Class MediaCest. + * + * Basic com_media (files) tests. + * + * @since __DEPLOY_VERSION__ + */ +class MediaCest +{ + /** + * 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)); + } + + // 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); + } + + /** + * Runs after every test. + * + * @param mixed ApiTester $I Api tester + * + * @since __DEPLOY_VERSION__ + * + * @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 + * + * @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 GET media adapter endpoint for a single adapter 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 GET 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); + $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 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. + * + * @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']]]); + $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']]]); + } + + /** + * 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; + } +}