diff --git a/administrator/components/com_media/.babelrc b/administrator/components/com_media/.babelrc index af0f0c3d35213..c970805e960e1 100644 --- a/administrator/components/com_media/.babelrc +++ b/administrator/components/com_media/.babelrc @@ -1,3 +1,4 @@ { - "presets": ["es2015"] + "presets": ["es2015"], + "plugins": ["transform-runtime"] } \ No newline at end of file diff --git a/administrator/components/com_media/libraries/media/file/adapter/interface.php b/administrator/components/com_media/Adapter/AdapterInterface.php similarity index 67% rename from administrator/components/com_media/libraries/media/file/adapter/interface.php rename to administrator/components/com_media/Adapter/AdapterInterface.php index 8ec7297628388..aeb5215d48788 100644 --- a/administrator/components/com_media/libraries/media/file/adapter/interface.php +++ b/administrator/components/com_media/Adapter/AdapterInterface.php @@ -7,6 +7,8 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ +namespace Joomla\Component\Media\Administrator\Adapter; + defined('_JEXEC') or die; /** @@ -14,7 +16,7 @@ * * @since __DEPLOY_VERSION__ */ -interface MediaFileAdapterInterface +interface AdapterInterface { /** * Returns the requested file or folder. The returned object @@ -30,14 +32,14 @@ interface MediaFileAdapterInterface * - width: The width, when available * - height: The height, when available * - * If the path doesn't exist a MediaFileAdapterFilenotfoundexception is thrown. + * If the path doesn't exist a FileNotFoundException is thrown. * * @param string $path The path to the file or folder * - * @return stdClass[] + * @return \stdClass * * @since __DEPLOY_VERSION__ - * @throws Exception + * @throws \Exception */ public function getFile($path = '/'); @@ -55,17 +57,16 @@ public function getFile($path = '/'); * - width: The width, when available * - height: The height, when available * - * If the path doesn't exist a MediaFileAdapterFilenotfoundexception is thrown. + * If the path doesn't exist a FileNotFoundException is thrown. * - * @param string $path The folder - * @param string $filter The filter + * @param string $path The folder * - * @return stdClass[] + * @return \stdClass[] * * @since __DEPLOY_VERSION__ - * @throws Exception + * @throws \Exception */ - public function getFiles($path = '/', $filter = ''); + public function getFiles($path = '/'); /** * Creates a folder with the given name in the given path. @@ -76,7 +77,7 @@ public function getFiles($path = '/', $filter = ''); * @return void * * @since __DEPLOY_VERSION__ - * @throws Exception + * @throws \Exception */ public function createFolder($name, $path); @@ -90,7 +91,7 @@ public function createFolder($name, $path); * @return void * * @since __DEPLOY_VERSION__ - * @throws Exception + * @throws \Exception */ public function createFile($name, $path, $data); @@ -104,7 +105,7 @@ public function createFile($name, $path, $data); * @return void * * @since __DEPLOY_VERSION__ - * @throws Exception + * @throws \Exception */ public function updateFile($name, $path, $data); @@ -116,7 +117,7 @@ public function updateFile($name, $path, $data); * @return void * * @since __DEPLOY_VERSION__ - * @throws Exception + * @throws \Exception */ public function delete($path); @@ -130,7 +131,7 @@ public function delete($path); * @return void * * @since __DEPLOY_VERSION__ - * @throws Exception + * @throws \Exception */ public function move($sourcePath, $destinationPath, $force = false); @@ -144,7 +145,57 @@ public function move($sourcePath, $destinationPath, $force = false); * @return void * * @since __DEPLOY_VERSION__ - * @throws Exception + * @throws \Exception */ public function copy($sourcePath, $destinationPath, $force = false); + + /** + * Returns a public url for the given path. This function can be used by the cloud + * adapter to publish the media file and create a permanent publicly accessible + * url. + * + * @param string $path The path to file + * + * @return string + * + * @since __DEPLOY_VERSION__ + * @throws \FileNotFoundException + */ + public function getUrl($path); + + /** + * Returns the name of the adapter. + * It will be shown in the Media Manager + * + * @return string + * + * @since __DEPLOY_VERSION__ + */ + public function getAdapterName(); + + /** + * Search for a pattern in a given path + * + * @param string $path The base path for the search + * @param string $needle The path to file + * @param bool $recursive Do a recursive search + * + * @return \stdClass[] + * + * @since __DEPLOY_VERSION__ + */ + public function search($path, $needle, $recursive); + + /** + * Returns a temporary url for the given path. + * This is used internally in media manager + * + * @param string $path The path to file + * + * @return string + * + * @since __DEPLOY_VERSION__ + * @throws \FileNotFoundException + */ + public function getTemporaryUrl($path); } diff --git a/administrator/components/com_media/Controller/ApiController.php b/administrator/components/com_media/Controller/ApiController.php new file mode 100644 index 0000000000000..d0b168174ecc3 --- /dev/null +++ b/administrator/components/com_media/Controller/ApiController.php @@ -0,0 +1,442 @@ +input->getMethod(); + + $this->task = $task; + $this->method = $method; + + try + { + // Check token for requests which do modify files (all except get requests) + if ($method !== 'GET' && !Session::checkToken('json')) + { + throw new \InvalidArgumentException(\JText::_('JINVALID_TOKEN'), 403); + } + + $doTask = strtolower($method) . ucfirst($task); + + // Record the actual task being fired + $this->doTask = $doTask; + + if (!in_array($this->doTask, $this->taskMap)) + { + throw new \Exception(\JText::sprintf('JLIB_APPLICATION_ERROR_TASK_NOT_FOUND', $task), 405); + } + + $data = $this->$doTask(); + + // Return the data + $this->sendResponse($data); + } + catch (FileNotFoundException $e) + { + $this->sendResponse($e, 404); + } + catch (FileExistsException $e) + { + $this->sendResponse($e, 409); + } + catch (\Exception $e) + { + $errorCode = 500; + + if ($e->getCode() > 0) + { + $errorCode = $e->getCode(); + } + + $this->sendResponse($e, $errorCode); + } + } + + /** + * Files Get Method + * + * Examples: + * + * - GET a list of folders below the root: + * index.php?option=com_media&task=api.files + * /api/files + * - GET a list of files and subfolders of a given folder: + * index.php?option=com_media&task=api.files&format=json&path=/sampledata/fruitshop + * /api/files/sampledata/fruitshop + * - GET a list of files and subfolders of a given folder for a given search term: + * use recursive=1 to search recursively in the working directory + * index.php?option=com_media&task=api.files&format=json&path=/sampledata/fruitshop&search=apple + * /api/files/sampledata/fruitshop?search=apple + * To look up in same working directory set flag recursive=0 + * index.php?option=com_media&task=api.files&format=json&path=/sampledata/fruitshop&search=apple&recursive=0 + * /api/files/sampledata/fruitshop?search=apple&recursive=0 + * - GET file information for a specific file: + * index.php?option=com_media&task=api.files&format=json&path=/sampledata/fruitshop/test.jpg + * /api/files/sampledata/fruitshop/test.jpg + * - GET a temporary URL to a given file + * index.php?option=com_media&task=api.files&format=json&path=/sampledata/fruitshop/test.jpg&url=1&temp=1 + * /api/files/sampledata/fruitshop/test.jpg&url=1&temp=1 + * - GET a temporary URL to a given file + * index.php?option=com_media&task=api.files&format=json&path=/sampledata/fruitshop/test.jpg&url=1 + * /api/files/sampledata/fruitshop/test.jpg&url=1 + * + * @return array The data to send with the response + * + * @since __DEPLOY_VERSION__ + * @throws \Exception + */ + public function getFiles() + { + // Grab options + $options = array(); + $options['url'] = $this->input->getBool('url', false); + $options['temp'] = $this->input->getBool('temp', false); + $options['search'] = $this->input->getString('search', ''); + $options['recursive'] = $this->input->getBool('recursive', true); + + return $this->getModel()->getFiles($this->getAdapter(), $this->getPath(), $options); + } + + /** + * Files delete Method + * + * Examples: + * + * - DELETE an existing folder in a specific folder: + * index.php?option=com_media&task=api.files&format=json&path=/sampledata/fruitshop/test + * /api/files/sampledata/fruitshop/test + * - DELETE an existing file in a specific folder: + * index.php?option=com_media&task=api.files&path=/sampledata/fruitshop/test.jpg + * /api/files/sampledata/fruitshop/test.jpg + * + * @return null + * + * @since __DEPLOY_VERSION__ + * @throws \Exception + */ + public function deleteFiles() + { + $this->getModel()->delete($this->getAdapter(), $this->getPath()); + + return null; + } + + /** + * Files Post Method + * + * Examples: + * + * - POST a new file or folder into a specific folder, the file or folder information is returned: + * index.php?option=com_media&task=api.files&format=json&path=/sampledata/fruitshop + * /api/files/sampledata/fruitshop + * + * New file body: + * { + * "name": "test.jpg", + * "content":"base64 encoded image" + * } + * New folder body: + * { + * "name": "test", + * } + * + * @return array The data to send with the response + * + * @since __DEPLOY_VERSION__ + * @throws \Exception + */ + public function postFiles() + { + $adapter = $this->getAdapter(); + $path = $this->getPath(); + $content = $this->input->json; + $name = $content->getString('name'); + $mediaContent = base64_decode($content->get('content', '', 'raw')); + $override = $content->get('override', false); + + $name = $this->getSafeName($name); + + if ($mediaContent) + { + $this->checkContent($name, $mediaContent); + + // A file needs to be created + $this->getModel()->createFile($adapter, $name, $path, $mediaContent, $override); + } + else + { + // A file needs to be created + $this->getModel()->createFolder($adapter, $name, $path, $override); + } + + return $this->getModel()->getFile($adapter, $path . '/' . $name); + } + + /** + * Files Put method + * + * Examples: + * + * - PUT a media file, the file or folder information is returned: + * index.php?option=com_media&task=api.files&format=json&path=/sampledata/fruitshop/test.jpg + * /api/files/sampledata/fruitshop/test.jpg + * + * Update file body: + * { + * "content":"base64 encoded image" + * } + * + * - PUT move a file, folder to another one + * path : will be taken as the source + * index.php?option=com_media&task=api.files&format=json&path=/sampledata/fruitshop/test.jpg + * /api/files/sampledata/fruitshop/test.jpg + * + * JSON body: + * { + * "newPath" : "/path/to/destination", + * "move" : "1" + * } + * + * - PUT copy a file, folder to another one + * path : will be taken as the source + * index.php?option=com_media&task=api.files&format=json&path=/sampledata/fruitshop/test.jpg + * /api/files/sampledata/fruitshop/test.jpg + * + * JSON body: + * { + * "newPath" : "/path/to/destination", + * "move" : "0" + * } + * + * @return array The data to send with the response + * + * @since __DEPLOY_VERSION__ + * @throws \Exception + */ + public function putFiles() + { + $adapter = $this->getAdapter(); + $path = $this->getPath(); + + $content = $this->input->json; + $name = basename($path); + $mediaContent = base64_decode($content->get('content', '', 'raw')); + $newPath = $content->getString('newPath', null); + $move = $content->get('move', true); + + if ($mediaContent != null) + { + $this->checkContent($name, $mediaContent); + + $this->getModel()->updateFile($adapter, $name, str_replace($name, '', $path), $mediaContent); + } + + if ($newPath != null) + { + list($destinationAdapter, $destinationPath) = explode(':', $newPath, 2); + + if ($move) + { + $this->getModel()->move($adapter, $path, $destinationPath, true); + } + else + { + $this->getModel()->copy($adapter, $path, $destinationPath, true); + } + + $path = $destinationPath; + } + + return $this->getModel()->getFile($adapter, $path); + } + + /** + * Send the given data as JSON response in the following format: + * + * {"success":true,"message":"ok","messages":null,"data":[{"type":"dir","name":"banners","path":"//"}]} + * + * @param mixed $data The data to send + * @param number $responseCode The response code + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + private function sendResponse($data = null, $responseCode = 200) + { + // Set the correct content type + $this->app->setHeader('Content-Type', 'application/json'); + + // Set the status code for the response + http_response_code($responseCode); + + // Send the data + echo new JsonResponse($data); + + $this->app->close(); + } + + /** + * Method to get a model object, loading it if required. + * + * @param string $name The model name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $config Configuration array for model. Optional. + * + * @return Model|boolean Model object on success; otherwise false on failure. + * + * @since __DEPLOY_VERSION__ + */ + public function getModel($name = 'Api', $prefix = 'Administrator', $config = array()) + { + return parent::getModel($name, $prefix, $config); + } + + /** + * Performs various check if it is allowed to save the content with the given name. + * + * @param string $name The filename + * @param string $mediaContent The media content + * + * @return void + * + * @since __DEPLOY_VERSION__ + * @throws \Exception + */ + private function checkContent($name, $mediaContent) + { + if (!Factory::getUser()->authorise('core.create', 'com_media')) + { + throw new \Exception(\JText::_('COM_MEDIA_ERROR_CREATE_NOT_PERMITTED'), 403); + } + + $params = ComponentHelper::getParams('com_media'); + + $helper = new MediaHelper; + $serverlength = $this->input->server->get('CONTENT_LENGTH'); + + if ($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 \Exception(\JText::_('COM_MEDIA_ERROR_WARNFILETOOLARGE'), 403); + } + + // @todo find a better way to check the input, by not writing the file to the disk + $tmpFile = $this->app->getConfig()->get('tmp_path') . '/' . uniqid() . $name; + + if (!\JFile::write($tmpFile, $mediaContent)) + { + throw new \Exception(\JText::_('JLIB_MEDIA_ERROR_UPLOAD_INPUT'), 500); + } + + $name = $this->getSafeName($name); + if (!$helper->canUpload(array('name' => $name, 'size' => count($mediaContent), 'tmp_name' => $tmpFile), 'com_media')) + { + \JFile::delete($tmpFile); + + throw new \Exception(\JText::_('COM_MEDIA_ERROR_UNABLE_TO_UPLOAD_FILE'), 403); + } + + \JFile::delete($tmpFile); + } + + /** + * Creates a safe file name for the given name. + * + * @param string $name The filename + * + * @return string + * + * @since __DEPLOY_VERSION__ + * @throws \Exception + */ + private function getSafeName($name) + { + // Make the filename safe + $name = \JFile::makeSafe($name); + + // Transform filename to punycode + $name = \JStringPunycode::toPunycode($name); + + $extension = \JFile::getExt($name); + + if ($extension) + { + $extension = '.' . strtolower($extension); + } + + // Transform filename to punycode, then neglect other than non-alphanumeric characters & underscores. + // Also transform extension to lowercase. + $nameWithoutExtension = substr($name, 0, strlen($name) - strlen($extension)); + $name = preg_replace(array("/[\\s]/", '/[^a-zA-Z0-9_]/'), array('_', ''), $nameWithoutExtension) . $extension; + + return $name; + } + + /** + * Get the Adapter + * + * @return string + * + * @since __DEPLOY_VERSION__ + */ + private function getAdapter() + { + return explode(':', $this->input->getString('path', ''), 2)[0]; + } + + /** + * Get the Path + * + * @return string + * + * @since __DEPLOY_VERSION__ + */ + private function getPath() + { + return explode(':', $this->input->getString('path', ''), 2)[1]; + } + +} diff --git a/administrator/components/com_media/Controller/DisplayController.php b/administrator/components/com_media/Controller/DisplayController.php new file mode 100644 index 0000000000000..12cf99261d46a --- /dev/null +++ b/administrator/components/com_media/Controller/DisplayController.php @@ -0,0 +1,66 @@ +input->getString('plugin', null); + $plugins = PluginHelper::getPlugin('filesystem'); + + // If plugin name was not found in parameters redirect back to control panel + if (!$pluginName || !$this->containsPlugin($plugins, $pluginName)) + { + throw new \Exception('Plugin not found!.', 'error'); + } + + // Check if the plugin is disabled, if so redirect to control panel + if (!PluginHelper::isEnabled('filesystem', $pluginName)) + { + throw new \Exception('Plugin ' . $pluginName . ' is disabled.', 'error'); + } + + // Only import our required plugin, not entire group + PluginHelper::importPlugin('filesystem', $pluginName); + + // Event parameters + $eventParameters = ['context' => $pluginName, 'input' => $this->input]; + $event = new OAuthCallbackEvent('onFileSystemOAuthCallback', $eventParameters); + + // Get results from event + $eventResults = (array) Factory::getApplication()->triggerEvent('onFileSystemOAuthCallback', $event); + + // If event was not triggered in the selected Plugin, raise a warning and fallback to Control Panel + if (!$eventResults) + { + throw new \Exception('Plugin ' . $pluginName . ' should have implemented ' + . 'onFileSystemOAuthCallback method'); + } + + $action = isset($eventResults['action']) ? $eventResults['action'] : null; + $message = null; + + // If there are any messages display them + if (isset($eventResults['message'])) + { + $message = $eventResults['message']; + $messageType = (isset($eventResults['message_type']) ? $eventResults['message_type'] : ''); + + Factory::getApplication()->enqueueMessage($message, $messageType); + } + + /** + * Execute actions defined by the plugin + * Supported actions + * - close : Closes the current window, use this only for windows opened by javascript + * - redirect : Redirect to a URI defined in 'redirect_uri' parameter, if not fallback to control panel + * - media-manager : Redirect to Media Manager + * - control-panel : Redirect to Control Panel + */ + switch ($action) + { + /** + * Close a window opened by developer + * Use this for close New Windows opened for OAuth Process + */ + case 'close': + $this->setRedirect(\JRoute::_('index.php?option=com_media&view=plugin&action=close', false)); + break; + + // Redirect browser to any page specified by the user + case 'redirect': + if (!isset($eventResults['redirect_uri'])) + { + throw new \Exception("Redirect URI must be set in the plugin"); + } + $this->setRedirect($eventResults['redirect_uri']); + break; + + // Redirect browser to Control Panel + case 'control-panel': + $this->setRedirect(\JRoute::_('index.php', false)); + break; + + // Redirect browser to Media Manager + case 'media-manager': + default: + $this->setRedirect(\JRoute::_('index.php?option=com_media', false)); + } + } + catch (\Exception $e) + { + // Display any error + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + $this->setRedirect(\JRoute::_('index.php', false)); + } + + // Redirect + $this->redirect(); + } + + /** + * Check whether a plugin exists in given plugin array. + * + * @param array $plugins Array of plugin names + * @param string $pluginName Plugin name to look up + * + * @return bool + * + * @since __DEPLOY_VERSION__ + */ + private function containsPlugin($plugins, $pluginName) + { + foreach ($plugins as $plugin) + { + if ($plugin->name == $pluginName) + { + return true; + } + } + + return false; + } +} diff --git a/administrator/components/com_media/Event/MediaProviderEvent.php b/administrator/components/com_media/Event/MediaProviderEvent.php new file mode 100644 index 0000000000000..dcae1e89288d7 --- /dev/null +++ b/administrator/components/com_media/Event/MediaProviderEvent.php @@ -0,0 +1,57 @@ +providerManager; + } + + /** + * Set the ProviderManager + * + * @param ProviderManager $providerManager The Provider Manager to be set + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public function setProviderManager($providerManager) + { + $this->providerManager = $providerManager; + } +} diff --git a/administrator/components/com_media/Event/OAuthCallbackEvent.php b/administrator/components/com_media/Event/OAuthCallbackEvent.php new file mode 100644 index 0000000000000..d83ca25a72087 --- /dev/null +++ b/administrator/components/com_media/Event/OAuthCallbackEvent.php @@ -0,0 +1,90 @@ +context; + } + + /** + * Set the event context. + * + * @param string $context Event context + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public function setContext($context) + { + $this->context = $context; + } + + /** + * Get the event input. + * + * @return \JInput + * + * @since __DEPLOY_VERSION__ + */ + public function getInput() + { + return $this->input; + } + + /** + * Set the event input. + * + * @param \JInput $input Event input + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public function setInput($input) + { + $this->input = $input; + } +} diff --git a/administrator/components/com_media/libraries/media/file/adapter/filenotfoundexception.php b/administrator/components/com_media/Exception/FileExistsException.php similarity index 70% rename from administrator/components/com_media/libraries/media/file/adapter/filenotfoundexception.php rename to administrator/components/com_media/Exception/FileExistsException.php index b4c5a38d086da..920ae621eeeab 100644 --- a/administrator/components/com_media/libraries/media/file/adapter/filenotfoundexception.php +++ b/administrator/components/com_media/Exception/FileExistsException.php @@ -7,14 +7,15 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ +namespace Joomla\Component\Media\Administrator\Exception; + defined('_JEXEC') or die; /** - * Media file adapter file not found exception. + * Media file exists exception. * * @since __DEPLOY_VERSION__ */ -class MediaFileAdapterFilenotfoundexception extends Exception +class FileExistsException extends \Exception { - } diff --git a/administrator/components/com_media/Exception/FileNotFoundException.php b/administrator/components/com_media/Exception/FileNotFoundException.php new file mode 100644 index 0000000000000..71287bca3b615 --- /dev/null +++ b/administrator/components/com_media/Exception/FileNotFoundException.php @@ -0,0 +1,21 @@ +providerManager == null) + { + $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); + } + + /** + * Returns the requested file or folder information. More information + * can be found in AdapterInterface::getFile(). + * + * @param string $adapter The adapter + * @param string $path The path to the file or folder + * @param array $options The options + * + * @return \stdClass + * + * @since __DEPLOY_VERSION__ + * @throws \Exception + * @see AdapterInterface::getFile() + */ + public function getFile($adapter, $path = '/', $options = array()) + { + // Add adapter prefix to the file returned + $file = $this->getAdapter($adapter)->getFile($path); + + // Check if it is a media file + if ($file->type == 'file' && !$this->isMediaFile($file->path)) + { + throw new InvalidPathException; + } + + if (isset($options['url']) && $options['url'] && $file->type == 'file') + { + if (isset($options['temp']) && $options['temp']) + { + $file->tempUrl = $this->getTemporaryUrl($adapter, $file->path); + } + else + { + $file->url = $this->getUrl($adapter, $file->path); + } + } + + $file->path = $adapter . ":" . $file->path; + $file->adapter = $adapter; + + return $file; + } + + /** + * Returns the folders and files for the given path. More information + * can be found in AdapterInterface::getFiles(). + * + * @param string $adapter The adapter + * @param string $path The folder + * @param array $options The options + * + * @return \stdClass[] + * + * @since __DEPLOY_VERSION__ + * @throws \Exception + * @see AdapterInterface::getFile() + */ + public function getFiles($adapter, $path = '/', $options = array()) + { + // Check whether user searching + if ($options['search'] != null) + { + // Do search + $files = $this->search($adapter, $options['search'], $path, $options['recursive']); + } + else + { + // Grab files for the path + $files = $this->getAdapter($adapter)->getFiles($path); + } + + // Add adapter prefix to all the files to be returned + foreach ($files as $key => $file) + { + // Check if the file is valid + if ($file->type == 'file' && !$this->isMediaFile($file->path)) + { + // Remove the file from the data + unset($files[$key]); + continue; + } + + // Check if we need more information + if (isset($options['url']) && $options['url'] && $file->type == 'file') + { + if (isset($options['temp']) && $options['temp']) + { + $file->tempUrl = $this->getTemporaryUrl($adapter, $file->path); + } + else + { + $file->url = $this->getUrl($adapter, $file->path); + } + } + + $file->path = $adapter . ":" . $file->path; + $file->adapter = $adapter; + } + + // Return array with proper indexes + return array_values($files); + } + + /** + * Creates a folder with the given name in the given path. More information + * can be found in AdapterInterface::createFolder(). + * + * @param string $adapter The adapter + * @param string $name The name + * @param string $path The folder + * @param boolean override Should the folder being overriden when it exists + * + * @return void + * + * @since __DEPLOY_VERSION__ + * @throws \Exception + * @see AdapterInterface::createFolder() + */ + public function createFolder($adapter, $name, $path, $override) + { + try + { + $file = $this->getFile($adapter, $path . '/' . $name); + } + catch (FileNotFoundException $e) + {} + + // Check if the file exists + if ($file && !$override) + { + throw new FileExistsException; + } + + $this->getAdapter($adapter)->createFolder($name, $path); + } + + /** + * Creates a file with the given name in the given path with the data. More information + * can be found in AdapterInterface::createFile(). + * + * @param string $adapter The adapter + * @param string $name The name + * @param string $path The folder + * @param binary $data The data + * @param boolean override Should the file being overriden when it exists + * + * @return void + * + * @since __DEPLOY_VERSION__ + * @throws \Exception + * @see AdapterInterface::createFile() + */ + public function createFile($adapter, $name, $path, $data, $override) + { + try + { + $file = $this->getFile($adapter, $path . '/' . $name); + } + catch (FileNotFoundException $e) + {} + + // Check if the file exists + if (isset($file) && !$override) + { + throw new FileExistsException; + } + + // Check if it is a media file + if (!$this->isMediaFile($path . '/' . $name)) + { + throw new InvalidPathException; + } + + $this->getAdapter($adapter)->createFile($name, $path, $data); + } + + /** + * Updates the file with the given name in the given path with the data. More information + * can be found in AdapterInterface::updateFile(). + * + * @param string $adapter The adapter + * @param string $name The name + * @param string $path The folder + * @param binary $data The data + * + * @return void + * + * @since __DEPLOY_VERSION__ + * @throws \Exception + * @see AdapterInterface::updateFile() + */ + public function updateFile($adapter, $name, $path, $data) + { + // Check if it is a media file + if (!$this->isMediaFile($path . '/' . $name)) + { + throw new InvalidPathException; + } + + $this->getAdapter($adapter)->updateFile($name, $path, $data); + } + + /** + * Deletes the folder or file of the given path. More information + * can be found in AdapterInterface::delete(). + * + * @param string $adapter The adapter + * @param string $path The path to the file or folder + * + * @return void + * + * @since __DEPLOY_VERSION__ + * @throws \Exception + * @see AdapterInterface::delete() + */ + public function delete($adapter, $path) + { + $file = $this->getFile($adapter, $path); + + // Check if it is a media file + if ($file->type == 'file' && !$this->isMediaFile($file->path)) + { + throw new InvalidPathException; + } + + $this->getAdapter($adapter)->delete($path); + } + + /** + * Copies file or folder from source path to destination path + * If forced, existing files/folders would be overwritten + * + * @param string $adapter The adapter + * @param string $sourcePath Source path of the file or folder (relative) + * @param string $destinationPath Destination path(relative) + * @param bool $force Force to overwrite + * + * @return void + * + * @since __DEPLOY_VERSION__ + * @throws \Exception + */ + public function copy($adapter, $sourcePath, $destinationPath, $force = false) + { + $this->getAdapter($adapter)->copy($sourcePath, $destinationPath, $force); + } + + /** + * Moves file or folder from source path to destination path + * If forced, existing files/folders would be overwritten + * + * @param string $adapter The adapter + * @param string $sourcePath Source path of the file or folder (relative) + * @param string $destinationPath Destination path(relative) + * @param bool $force Force to overwrite + * + * @return void + * + * @since __DEPLOY_VERSION__ + * @throws \Exception + */ + public function move($adapter, $sourcePath, $destinationPath, $force = false) + { + $this->getAdapter($adapter)->move($sourcePath, $destinationPath, $force); + } + + /** + * Returns an url for serve media files from adapter. + * Url must provide a valid image type to be displayed on Joomla! site. + * + * @param string $adapter The adapter + * @param string $path The relative path for the file + * + * @return string Permalink to the relative file + * + * @since __DEPLOY_VERSION__ + * @throws FileNotFoundException + */ + public function getUrl($adapter, $path) + { + // Check if it is a media file + if (!$this->isMediaFile($path)) + { + throw new InvalidPathException; + } + + return $this->getAdapter($adapter)->getUrl($path); + } + + /** + * Search for a pattern in a given path + * + * @param string $adapter The adapter to work on + * @param string $needle The search therm + * @param string $path The base path for the search + * @param bool $recursive Do a recursive search + * + * @return \stdClass[] + * + * @since __DEPLOY_VERSION__ + * @throws \Exception + */ + public function search($adapter, $needle, $path = '/', $recursive = true) + { + return $this->getAdapter($adapter)->search($path, $needle, $recursive); + } + + /** + * Returns a temporary url for the given path. + * This is used internally in media manager + * + * @param string $adapter The adapter + * @param string $path The path to file + * + * @return string + * + * @since __DEPLOY_VERSION__ + * @throws \Exception + */ + public function getTemporaryUrl($adapter, $path) + { + // Check if it is a media file + if (!$this->isMediaFile($path)) + { + throw new InvalidPathException; + } + + return $this->getAdapter($adapter)->getTemporaryUrl($path); + } + + /** + * Checks if the given path is an allowed media file. + * + * @param string $path The path to file + * + * @return boolean + * + * @since __DEPLOY_VERSION__ + */ + private function isMediaFile($path) + { + // Check if there is an extension available + if (!strrpos($path, '.')) + { + return false; + } + + // Initialize the allowed extensions + if ($this->allowedExtensions === null) { + + // Get the setting from the params + $this->allowedExtensions = ComponentHelper::getParams('com_media')->get( + 'upload_extensions', + 'bmp,csv,doc,gif,ico,jpg,jpeg,odg,odp,ods,odt,pdf,png,ppt,txt,xcf,xls,BMP,CSV,DOC,GIF,ICO,JPG,JPEG,ODG,ODP,ODS,ODT,PDF,PNG,PPT,TXT,XCF,XLS' + ); + + // Make them an array + $this->allowedExtensions = explode(',', $this->allowedExtensions); + } + + // Extract the extension + $extension = substr($path, strrpos($path, '.') + 1); + + // Check if the extension exists in the allowed extensions + return in_array($extension, $this->allowedExtensions); + } +} diff --git a/administrator/components/com_media/models/file.php b/administrator/components/com_media/Model/FileModel.php similarity index 51% rename from administrator/components/com_media/models/file.php rename to administrator/components/com_media/Model/FileModel.php index 1a4a1055c2b29..1dd3262d73bba 100644 --- a/administrator/components/com_media/models/file.php +++ b/administrator/components/com_media/Model/FileModel.php @@ -7,16 +7,19 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ +namespace Joomla\Component\Media\Administrator\Model; + defined('_JEXEC') or die; use Joomla\CMS\MVC\Model\FormModel; +use Joomla\CMS\Plugin\PluginHelper; /** * File Model * * @since __DEPLOY_VERSION__ */ -class MediaModelFile extends FormModel +class FileModel extends FormModel { /** * Method to get the record form. @@ -24,13 +27,13 @@ class MediaModelFile extends FormModel * @param array $data Data for the form. * @param boolean $loadData True if the form is to load its own data (default case), false if not. * - * @return JForm|boolean A JForm object on success, false on failure + * @return \Joomla\CMS\Form\Form|boolean A Form object on success, false on failure * - * @since 1.6 + * @since __DEPLOY_VERSION__ */ public function getForm($data = array(), $loadData = true) { - JPluginHelper::importPlugin('media-action'); + PluginHelper::importPlugin('media-action'); // Get the form. $form = $this->loadForm('com_media.file', 'file', array('control' => 'jform', 'load_data' => $loadData)); @@ -42,4 +45,22 @@ public function getForm($data = array(), $loadData = true) return $form; } + + /** + * Method to get the file information for the given path. Path must be + * in the format: adapter:path/to/file.extension + * + * @param string $path The path to get the information from. + * + * @return \stdClass A object with file information + * + * @since __DEPLOY_VERSION__ + * @see ApiModel::getFile() + */ + public function getFileInformation($path) + { + list($adapter, $path) = explode(':', $path, 2); + + return (new ApiModel)->getFile($adapter, $path, ['url' => true]); + } } diff --git a/administrator/components/com_media/Model/MediaModel.php b/administrator/components/com_media/Model/MediaModel.php new file mode 100644 index 0000000000000..55fe893be674d --- /dev/null +++ b/administrator/components/com_media/Model/MediaModel.php @@ -0,0 +1,62 @@ + 'AdapterManager', 'providerManager' => $providerManager]; + $event = new MediaProviderEvent('onSetupProviders', $eventParameters); + $results = []; + + // Import plugin group and fire the event + PluginHelper::importPlugin('filesystem'); + Factory::getApplication()->triggerEvent('onSetupProviders', $event); + + foreach ($providerManager->getProviders() as $provider) + { + $result = new \stdClass; + $result->name = $provider->getID(); + $result->displayName = $provider->getDisplayName(); + + foreach ($provider->getAdapters() as $adapter) + { + $result->adapterNames[] = $adapter->getAdapterName(); + } + + $results[] = $result; + } + + return $results; + } +} diff --git a/administrator/components/com_media/libraries/media/plugin/mediaaction.php b/administrator/components/com_media/Plugin/MediaActionPlugin.php similarity index 84% rename from administrator/components/com_media/libraries/media/plugin/mediaaction.php rename to administrator/components/com_media/Plugin/MediaActionPlugin.php index 561db821e0b2a..8120cc5acb5a1 100644 --- a/administrator/components/com_media/libraries/media/plugin/mediaaction.php +++ b/administrator/components/com_media/Plugin/MediaActionPlugin.php @@ -7,15 +7,19 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ +namespace Joomla\Component\Media\Administrator\Plugin; + defined('_JEXEC') or die; +use Joomla\CMS\Form\Form; +use Joomla\CMS\Plugin\CMSPlugin; /** * Media Manager Base Plugin for the media actions * * @since __DEPLOY_VERSION__ */ -class MediaActionPlugin extends JPlugin +class MediaActionPlugin extends CMSPlugin { /** * Load the language file on instantiation. @@ -30,14 +34,14 @@ class MediaActionPlugin extends JPlugin * The form event. Load additional parameters when available into the field form. * Only when the type of the form is of interest. * - * @param JForm $form The form - * @param stdClass $data The data + * @param Form $form The form + * @param \stdClass $data The data * * @return void * * @since __DEPLOY_VERSION__ */ - public function onContentPrepareForm(JForm $form, $data) + public function onContentPrepareForm(Form $form, $data) { // Check if it is the right form if ($form->getName() != 'com_media.file') @@ -67,7 +71,7 @@ public function onContentPrepareForm(JForm $form, $data) */ protected function loadJs() { - JHtml::_( + \JHtml::_( 'script', 'plg_media-action_' . $this->_name . '/' . $this->_name . '.js', array('version' => 'auto', 'relative' => true) @@ -83,7 +87,7 @@ protected function loadJs() */ protected function loadCss() { - JHtml::_( + \JHtml::_( 'stylesheet', 'plg_media-action_' . $this->_name . '/' . $this->_name . '.css', array('version' => 'auto', 'relative' => true) diff --git a/administrator/components/com_media/Provider/ProviderInterface.php b/administrator/components/com_media/Provider/ProviderInterface.php new file mode 100644 index 0000000000000..e000b8586af81 --- /dev/null +++ b/administrator/components/com_media/Provider/ProviderInterface.php @@ -0,0 +1,49 @@ +providers; + } + + /** + * Register a provider into the ProviderManager + * + * @param ProviderInterface $provider The provider to be registered + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public function registerProvider(ProviderInterface $provider) + { + $this->providers[$provider->getID()] = $provider; + } + + /** + * Returns the provider for a particular ID + * + * @param string $id The ID for the provider + * + * @return ProviderInterface + * + * @throws \Exception + * + * @since __DEPLOY_VERSION__ + */ + public function getProvider($id) + { + if (!isset($this->providers[$id])) + { + throw new \Exception("Media Provider not found"); + } + + return $this->providers[$id]; + } + + /** + * Returns an adapter for an account + * + * @param string $name The name of an adapter + * + * @return AdapterInterface + * + * @throws \Exception + * + * @since __DEPLOY_VERSION__ + */ + public function getAdapter($name) + { + list($provider, $account) = array_pad(explode('-', $name, 2), 2, null); + + if ($account == null) + { + throw new \Exception('Account was not set'); + } + + $adapters = $this->getProvider($provider)->getAdapters(); + + if (!isset($adapters[$account])) + { + throw new \Exception("The account was not found"); + } + + return $adapters[$account]; + } +} diff --git a/administrator/components/com_media/View/File/HtmlView.php b/administrator/components/com_media/View/File/HtmlView.php new file mode 100644 index 0000000000000..8291c57ba70b6 --- /dev/null +++ b/administrator/components/com_media/View/File/HtmlView.php @@ -0,0 +1,79 @@ +input; + + $this->form = $this->get('Form'); + + // The component params + $this->params = ComponentHelper::getParams('com_media'); + + // The requested file + $this->file = $this->getModel()->getFileInformation($input->getString('path', null)); + + // At the moment we only support local files to edit + if (strpos($this->file->adapter, 'local-') !== 0) + { + // @todo error handling controller redirect files + throw new \Exception('Image file is not locally'); + } + + $this->addToolbar(); + + return parent::display($tpl); + } + + /** + * Add the toolbar buttons + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + protected function addToolbar() + { + ToolbarHelper::title(\JText::_('COM_MEDIA_EDIT'), 'images mediamanager'); + + ToolbarHelper::apply('apply'); + ToolbarHelper::save('save'); + ToolbarHelper::custom('reset', 'refresh', '', 'COM_MEDIA_RESET', false); + + ToolbarHelper::cancel('cancel'); + } +} diff --git a/administrator/components/com_media/View/Media/HtmlView.php b/administrator/components/com_media/View/Media/HtmlView.php new file mode 100644 index 0000000000000..901fc90b43b1e --- /dev/null +++ b/administrator/components/com_media/View/Media/HtmlView.php @@ -0,0 +1,125 @@ +prepareToolbar(); + + // Get enabled adapters + $this->providers = $this->get('Providers'); + + // Check that there are providers + if (!count($this->providers)) + { + // TODO throw an exception + } + + $this->currentPath = Factory::getApplication()->input->getString('path'); + + parent::display($tpl); + } + + /** + * Prepare the toolbar. + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + protected function prepareToolbar() + { + $tmpl = Factory::getApplication()->input->getCmd('tmpl'); + + // Get the toolbar object instance + $bar = Toolbar::getInstance('toolbar'); + $user = Factory::getUser(); + + // Set the title + ToolbarHelper::title(\JText::_('COM_MEDIA'), 'images mediamanager'); + + // Add the upload and create folder buttons + if ($user->authorise('core.create', 'com_media')) + { + // Add the upload button + $layout = new FileLayout('toolbar.upload', JPATH_COMPONENT_ADMINISTRATOR . '/layouts'); + + $bar->appendButton('Custom', $layout->render(array()), 'upload'); + ToolbarHelper::divider(); + + // Add the create folder button + $layout = new FileLayout('toolbar.create-folder', JPATH_COMPONENT_ADMINISTRATOR . '/layouts'); + + $bar->appendButton('Custom', $layout->render(array()), 'new'); + ToolbarHelper::divider(); + } + + // Add a delete button + if ($user->authorise('core.delete', 'com_media')) + { + // Instantiate a new JLayoutFile instance and render the layout + $layout = new FileLayout('toolbar.delete'); + + $bar->appendButton('Custom', $layout->render(array()), 'delete'); + ToolbarHelper::divider(); + } + + // Add the preferences button + if (($user->authorise('core.admin', 'com_media') || $user->authorise('core.options', 'com_media')) && $tmpl !== 'component') + { + ToolbarHelper::preferences('com_media'); + ToolbarHelper::divider(); + } + + if ($tmpl !== 'component') + { + ToolbarHelper::help('JHELP_CONTENT_MEDIA_MANAGER'); + } + } +} diff --git a/administrator/components/com_media/config.xml b/administrator/components/com_media/config.xml index eb905ee269465..06a9a57fd3d00 100644 --- a/administrator/components/com_media/config.xml +++ b/administrator/components/com_media/config.xml @@ -34,7 +34,7 @@ name="file_path" type="text" label="COM_MEDIA_FIELD_PATH_FILE_FOLDER_LABEL" - description="COM_MEDIA_FIELD_PATH_FILE_FOLDER_DESC" + description="COM_MEDIA_FIELD_PATH_FILE_FOLDER_DESC" size="50" default="images" /> diff --git a/administrator/components/com_media/controllers/api.json.php b/administrator/components/com_media/controllers/api.json.php deleted file mode 100644 index 5af92e088a57c..0000000000000 --- a/administrator/components/com_media/controllers/api.json.php +++ /dev/null @@ -1,251 +0,0 @@ -input->getPath('path', '/', 'path'); - - // Determine the method - $method = strtolower($this->input->getMethod() ? : 'GET'); - - try - { - // Check token for requests which do modify files (all except get requests) - if ($method != 'get' && !JSession::checkToken('json')) - { - throw new InvalidArgumentException(JText::_('JINVALID_TOKEN'), 403); - } - - // Gather the data according to the method - switch ($method) - { - case 'get': - $data = $this->getModel()->getFiles($path, $this->input->getWord('filter')); - break; - case 'delete': - $this->getModel()->delete($path); - - // Define this for capability with other cases - $data = null; - break; - case 'post': - $content = $this->input->json; - $name = $content->getString('name'); - $mediaContent = base64_decode($content->get('content', '', 'raw')); - - if ($mediaContent) - { - $this->checkContent($name, $mediaContent); - - // A file needs to be created - $name = $this->getModel()->createFile($name, $path, $mediaContent); - } - else - { - // A file needs to be created - $name = $this->getModel()->createFolder($name, $path); - } - - $data = $this->getModel()->getFile($path . '/' . $name); - break; - case 'put': - $content = $this->input->json; - $name = basename($path); - $mediaContent = base64_decode($content->get('content', '', 'raw')); - - $this->checkContent($name, $mediaContent); - - $this->getModel()->updateFile($name, str_replace($name, '', $path), $mediaContent); - - $data = $this->getModel()->getFile($path); - break; - default: - throw new BadMethodCallException('Method not supported yet!'); - } - - // Return the data - $this->sendResponse($data); - } - catch (MediaFileAdapterFilenotfoundexception $e) - { - $this->sendResponse($e, 404); - } - catch (Exception $e) - { - $errorCode = 500; - - if ($e->getCode() > 0) - { - $errorCode = $e->getCode(); - } - $this->sendResponse($e, $errorCode); - } - } - - /** - * Send the given data as JSON response in the following format: - * - * {"success":true,"message":"ok","messages":null,"data":[{"type":"dir","name":"banners","path":"//"}]} - * - * @param mixed $data The data to send - * @param number $responseCode The response code - * - * @return void - * - * @since __DEPLOY_VERSION__ - */ - protected function sendResponse($data = null, $responseCode = 200) - { - // Set the correct content type - JFactory::getApplication()->setHeader('Content-Type', 'application/json'); - - // Set the status code for the response - http_response_code($responseCode); - - // Send the data - echo new JResponseJson($data); - } - - /** - * Method to get a model object, loading it if required. - * - * @param string $name The model name. Optional. - * @param string $prefix The class prefix. Optional. - * @param array $config Configuration array for model. Optional. - * - * @return Model|boolean Model object on success; otherwise false on failure. - * - * @since 3.0 - */ - public function getModel($name = 'Api', $prefix = 'MediaModel', $config = array()) - { - return parent::getModel($name, $prefix, $config); - } - - /** - * Performs various check if it is allowed to save the content with the given name. - * - * @param string $name The filename - * @param string $mediaContent The media content - * - * @return void - * - * @since __DEPLOY_VERSION__ - * @throws Exception - */ - private function checkContent($name, $mediaContent) - { - if (!JFactory::getUser()->authorise('core.create', 'com_media')) - { - throw new Exception(JText::_('COM_MEDIA_ERROR_CREATE_NOT_PERMITTED'), 403); - } - - $params = JComponentHelper::getParams('com_media'); - - $helper = new JHelperMedia; - $serverlength = $this->input->server->get('CONTENT_LENGTH'); - - if ($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 Exception(JText::_('COM_MEDIA_ERROR_WARNFILETOOLARGE')); - } - - // @todo find a better way to check the input, by not writing the file to the disk - $tmpFile = JFactory::getApplication()->getConfig()->get('tmp_path') . '/' . uniqid($name); - - if (!JFile::write($tmpFile, $mediaContent)) - { - throw new Exception(JText::_('JLIB_MEDIA_ERROR_UPLOAD_INPUT')); - } - - if (!$helper->canUpload(array('name' => $name, 'size' => count($mediaContent), 'tmp_name' => $tmpFile), 'com_media')) - { - JFile::delete($tmpFile); - - throw new Exception(JText::_('COM_MEDIA_ERROR_UNABLE_TO_UPLOAD_FILE'), 403); - } - - JFile::delete($tmpFile); - } -} diff --git a/administrator/components/com_media/dispatcher.php b/administrator/components/com_media/dispatcher.php new file mode 100644 index 0000000000000..3338b4370b5d7 --- /dev/null +++ b/administrator/components/com_media/dispatcher.php @@ -0,0 +1,53 @@ +app->getIdentity(); + $asset = $this->input->get('asset'); + $author = $this->input->get('author'); + + // Access check + if (!$user->authorise('core.manage', 'com_media') + && (!$asset || (!$user->authorise('core.edit', $asset) + && !$user->authorise('core.create', $asset) + && count($user->getAuthorisedCategories($asset, 'core.create')) == 0) + && !($user->id == $author && $user->authorise('core.edit.own', $asset)))) + { + throw new \Joomla\CMS\Access\Exception\Notallowed(JText::_('JERROR_ALERTNOAUTHOR'), 403); + } + } +} diff --git a/administrator/components/com_media/models/forms/file.xml b/administrator/components/com_media/forms/file.xml similarity index 100% rename from administrator/components/com_media/models/forms/file.xml rename to administrator/components/com_media/forms/file.xml diff --git a/administrator/components/com_media/layouts/toolbar/create-folder.php b/administrator/components/com_media/layouts/toolbar/create-folder.php index a5fc8062773bf..d82b9e00b3cf9 100644 --- a/administrator/components/com_media/layouts/toolbar/create-folder.php +++ b/administrator/components/com_media/layouts/toolbar/create-folder.php @@ -6,7 +6,9 @@ * @copyright Copyright (C) 2005 - 2017 Open Source Matters, Inc. All rights reserved. * @license GNU General Public License version 2 or later; see LICENSE.txt */ + defined('_JEXEC') or die; + $title = JText::_('COM_MEDIA_CREATE_NEW_FOLDER'); ?>