diff --git a/libraries/import.legacy.php b/libraries/import.legacy.php index 007101918705a..b7abe02a7e220 100644 --- a/libraries/import.legacy.php +++ b/libraries/import.legacy.php @@ -51,6 +51,11 @@ // Setup the autoloaders. JLoader::setup(); +// Register the extension root paths. +JLoader::registerExtensionRootFolder('', JPATH_SITE); +JLoader::registerExtensionRootFolder('Site', JPATH_SITE); +JLoader::registerExtensionRootFolder('Administrator', JPATH_ADMINISTRATOR); + JLoader::registerPrefix('J', JPATH_PLATFORM . '/legacy'); // Check if the JsonSerializable interface exists already diff --git a/libraries/loader.php b/libraries/loader.php index 17814d1faf943..9a92da9d701c2 100644 --- a/libraries/loader.php +++ b/libraries/loader.php @@ -72,6 +72,14 @@ abstract class JLoader */ protected static $deprecatedAliases = array(); + /** + * The root folders where extensions can be found. + * + * @var array + * @since __DEPLOY_VERSION__ + */ + protected static $extensionRootFolders = array(); + /** * Method to discover classes of a given type in a given path. * @@ -461,6 +469,23 @@ public static function registerNamespace($namespace, $path, $reset = false, $pre } } + /** + * Root folders where extensions can be found. For example: + * JLoader::registerExtensionRootFolder(JPATH_SITE, 'Site'); + * JLoader::registerExtensionRootFolder(JPATH_ADMINISTRATOR, 'Administrator'); + * + * @param string $key The key. + * @param string $path A absolute file path to the root where extensions can be found. + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public static function registerExtensionRootFolder($key, $path) + { + self::$extensionRootFolders[$key] = $path; + } + /** * Method to setup the autoloaders for the Joomla Platform. * Since the SPL autoloaders are called in a queue we will add our explicit @@ -498,10 +523,106 @@ public static function setup($enablePsr = true, $enablePrefixes = true, $enableC // Register the PSR based autoloader. spl_autoload_register(array('JLoader', 'loadByPsr0')); spl_autoload_register(array('JLoader', 'loadByPsr4')); + spl_autoload_register(array('JLoader', 'loadByExtension')); spl_autoload_register(array('JLoader', 'loadByAlias')); } } + /** + * Method to autoload classes that are namespaced and do belong to an extension. The + * extension must have the following pattern to be autoloaded: + * - Component: Joomla\Component\Content\Site + * - Module: Joomla\Module\ArticlesLatest\Administrator + * - Plugin: Joomla\Plugin\System\Cache + * + * @param string $class The fully qualified class name to autoload. + * + * @return boolean True on success, false otherwise. + * + * @since __DEPLOY_VERSION__ + */ + public static function loadByExtension($class) + { + // Check if it is a namespaced class + if (strrpos($class, '\\') === false) + { + return false; + } + + // Splice into segments + $segments = explode('\\', $class); + + // Check if there are enough segments + if (count($segments) < 5) + { + return false; + } + + // Check if it is an extension class + if (!in_array($segments[1], array('Component', 'Module', 'Plugin'))) + { + return false; + } + + // Normally Administrator or Site + $key = $segments[3]; + + // If it is a plugin, then the key is empty + if ($segments[1] == 'Plugin') + { + $key = ''; + } + + // Check if it is an extension class + if (!array_key_exists($key, self::$extensionRootFolders)) + { + return false; + } + + // Define the root of the path + $path = self::$extensionRootFolders[$key]; + + // Add the extension specific folder to the path + switch ($segments[1]) + { + case 'Component': + $name = strtolower($segments[2]); + $path .= '/components/com_' . $name; + break; + case 'Module': + // Convert the name of the extension from camelcase to underscore for module + $name = strtolower(implode('_', self::fromCamelCase($segments[2], true))); + $path .= '/modules/mod_' . $name; + break; + case 'Plugin': + $group = strtolower($segments[2]); + $name = strtolower($segments[3]); + $path .= '/plugins/' . $group . '/' . $name; + break; + } + + // Check if the extension supports a nice and clean folder structure + if (file_exists($path . '/src')) + { + $path .= '/src'; + } + + // Extension can't be autoloaded + if (!file_exists($path)) + { + return false; + } + + // Compile the namespace + $ns = implode('\\', array_slice($segments, 0, 4)); + + // Register the namespace + self::registerNamespace($ns, $path, false, false, 'psr4'); + + // Load the class by default PSR-4 routine + return self::loadByPsr4($class); + } + /** * Method to autoload classes that are namespaced to the PSR-4 standard. * @@ -795,6 +916,23 @@ private static function stripFirstBackslash($class) { return $class && $class[0] === '\\' ? substr($class, 1) : $class; } + + /** + * Copied form Normalise class, JLoader should not have an external dependency. + * + * @param string $input The string input (ASCII only). + * @param boolean $grouped Optionally allows splitting on groups of uppercase characters. + * + * @return string The space separated string. + * + * @since __DEPLOY_VERSION__ + */ + private static function fromCamelCase($input, $grouped = false) + { + return $grouped + ? preg_split('/(?<=[^A-Z_])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][^A-Z_])/x', $input) + : trim(preg_replace('#([A-Z])#', ' $1', $input)); + } } // Check if jexit is defined first (our unit tests mock this) diff --git a/libraries/src/Component/ComponentHelper.php b/libraries/src/Component/ComponentHelper.php index 9baa593d5cf56..d9da70259ef10 100644 --- a/libraries/src/Component/ComponentHelper.php +++ b/libraries/src/Component/ComponentHelper.php @@ -13,6 +13,7 @@ use Joomla\CMS\Access\Access; use Joomla\CMS\Component\Exception\MissingComponentException; use Joomla\Registry\Registry; +use Joomla\CMS\Dispatcher\DispatcherInterface; /** * Component helper class @@ -339,22 +340,46 @@ public static function renderComponent($option, $params = array()) define('JPATH_COMPONENT_ADMINISTRATOR', JPATH_ADMINISTRATOR . '/components/' . $option); } - $path = JPATH_COMPONENT . '/' . $file . '.php'; - // If component is disabled throw error - if (!static::isEnabled($option) || !file_exists($path)) + if (!static::isEnabled($option)) { throw new MissingComponentException(\JText::_('JLIB_APPLICATION_ERROR_COMPONENT_NOT_FOUND'), 404); } - // Load common and local language files. - $lang->load($option, JPATH_BASE, null, false, true) || $lang->load($option, JPATH_COMPONENT, null, false, true); - // Handle template preview outlining. $contents = null; - // Execute the component. - $contents = static::executeComponent($path); + // Check if we have a dispatcher + if (file_exists(JPATH_COMPONENT . '/dispatcher.php')) + { + require_once JPATH_COMPONENT . '/dispatcher.php'; + $class = ucwords($file) . 'Dispatcher'; + + // Check the class exists and implements the dispatcher interface + if (!class_exists($class) || !in_array('Joomla\\CMS\\Dispatcher\\DispatcherInterface', class_implements($class))) + { + throw new \LogicException(\JText::sprintf('JLIB_APPLICATION_ERROR_APPLICATION_LOAD', $option), 500); + } + + // Dispatch the component. + $contents = static::dispatchComponent(new $class($app, $app->input)); + } + else + { + $path = JPATH_COMPONENT . '/' . $file . '.php'; + + // If component file doesn't exist throw error + if (!file_exists($path)) + { + throw new \Exception(\JText::_('JLIB_APPLICATION_ERROR_COMPONENT_NOT_FOUND'), 404); + } + + // Load common and local language files. + $lang->load($option, JPATH_BASE, null, false, true) || $lang->load($option, JPATH_COMPONENT, null, false, true); + + // Execute the component. + $contents = static::executeComponent($path); + } // Revert the scope $app->scope = $scope; @@ -399,6 +424,24 @@ protected static function _load($option) return static::load($option); } + /** + * Dispatch the component. + * + * @param DispatcherInterface $dispatcher The dispatcher class. + * + * @return string The component output + * + * @since __DEPLOY_VERSION__ + */ + protected static function dispatchComponent(DispatcherInterface $dispatcher) + { + ob_start(); + $dispatcher->dispatch(); + $contents = ob_get_clean(); + + return $contents; + } + /** * Load the installed components into the components property. * @@ -499,4 +542,30 @@ public static function getComponents() return static::$components; } + + /** + * Returns the component name (eg. com_content) for the given object based on the class name. + * If the object is not namespaced, then the alternative name is used. + * + * @param object $object The object controller or model + * @param string $alternativeName Mostly the value of getName() from the object + * + * @return string The name + * + * @since __DEPLOY_VERSION__ + */ + public static function getComponentName($object, $alternativeName) + { + $reflect = new \ReflectionClass($object); + + if (!$reflect->getNamespaceName()) + { + return 'com_' . strtolower($alternativeName); + } + + $from = strpos($reflect->getNamespaceName(), '\\Component'); + $to = strpos(substr($reflect->getNamespaceName(), $from + 11), '\\'); + + return 'com_' . strtolower(substr($reflect->getNamespaceName(), $from + 11, $to)); + } } diff --git a/libraries/src/Dispatcher/Dispatcher.php b/libraries/src/Dispatcher/Dispatcher.php new file mode 100644 index 0000000000000..a96614a7fbb3c --- /dev/null +++ b/libraries/src/Dispatcher/Dispatcher.php @@ -0,0 +1,209 @@ +namespace)) + { + throw new \RuntimeException('Namespace can not be empty!'); + } + + $this->app = $app; + $this->input = $input ?: $app->input; + + // If option is not provided, detect it from dispatcher class name, ie ContentDispatcher + if (empty($this->option)) + { + $className = get_class($this); + $pos = strpos($className, 'Dispatcher'); + + if ($pos !== false) + { + $this->option = 'com_' . strtolower(substr($className, 0, $pos)); + } + } + + $this->loadLanguage(); + } + + /** + * Load the language + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + protected function loadLanguage() + { + // Load common and local language files. + $this->app->getLanguage()->load($this->option, JPATH_BASE, null, false, true) || + $this->app->getLanguage()->load($this->option, JPATH_COMPONENT, null, false, true); + } + + /** + * Method to check component access permission + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + protected function checkAccess() + { + // Check the user has permission to access this component if in the backend + if ($this->app->isClient('administrator') && !$this->app->getIdentity()->authorise('core.manage', $this->option)) + { + throw new NotAllowed($this->app->getLanguage()->_('JERROR_ALERTNOAUTHOR'), 403); + } + } + + /** + * Dispatch a controller task. Redirecting the user if appropriate. + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public function dispatch() + { + // Check component access permission + $this->checkAccess(); + + $command = $this->input->getCmd('task', 'display'); + + // Check for a controller.task command. + if (strpos($command, '.') !== false) + { + // Explode the controller.task command. + list ($controller, $task) = explode('.', $command); + + $this->input->set('controller', $controller); + $this->input->set('task', $task); + } + else + { + // Do we have a controller? + $controller = $this->input->get('controller', 'display'); + $task = $command; + } + + // Build controller config data + $config['option'] = $this->option; + + // Set name of controller if it is passed in the request + if ($this->input->exists('controller')) + { + $config['name'] = strtolower($this->input->get('controller')); + } + + // Execute the task for this component + $controller = $this->getController($controller, ucfirst($this->app->getName()), $config); + $controller->execute($task); + $controller->redirect(); + } + + /** + * The application the dispatcher is working with. + * + * @return CMSApplication + * + * @since __DEPLOY_VERSION__ + */ + protected function getApplication() + { + return $this->app; + } + + /** + * Get a controller from the component + * + * @param string $name Controller name + * @param string $client Optional client (like Administrator, Site etc.) + * @param array $config Optional controller config + * + * @return BaseController + * + * @since __DEPLOY_VERSION__ + */ + public function getController($name, $client = '', array $config = array()) + { + // Set up the namespace + $namespace = rtrim($this->namespace, '\\') . '\\'; + + // Set up the client + $client = $client ?: ucfirst($this->app->getName()); + + $controllerClass = $namespace . $client . '\\Controller\\' . ucfirst($name) . 'Controller'; + + if (!class_exists($controllerClass)) + { + throw new \InvalidArgumentException(\JText::sprintf('JLIB_APPLICATION_ERROR_INVALID_CONTROLLER_CLASS', $controllerClass)); + } + + return new $controllerClass($config, new MVCFactory($namespace, $this->app), $this->app, $this->input); + } +} diff --git a/libraries/src/Dispatcher/DispatcherInterface.php b/libraries/src/Dispatcher/DispatcherInterface.php new file mode 100644 index 0000000000000..8db4c59613885 --- /dev/null +++ b/libraries/src/Dispatcher/DispatcherInterface.php @@ -0,0 +1,29 @@ +option)) { - $this->option = 'com_' . strtolower($this->getName()); + $this->option = ComponentHelper::getComponentName($this, $this->getName()); } // Guess the \JText message prefix. Defaults to the option. @@ -92,9 +97,15 @@ public function __construct($config = array(), MVCFactoryInterface $factory = nu // Guess the list view as the suffix, eg: OptionControllerSuffix. if (empty($this->view_list)) { - $r = null; + $reflect = new \ReflectionClass($this); - if (!preg_match('/(.*)Controller(.*)/i', get_class($this), $r)) + $r = array(0 => '', 1 => '', 2 => $reflect->getShortName()); + + if ($reflect->getNamespaceName()) + { + $r[2] = str_replace('Controller', '', $r[2]); + } + elseif (!preg_match('/(.*)Controller(.*)/i', $reflect->getShortName(), $r)) { throw new \Exception(\JText::_('JLIB_APPLICATION_ERROR_CONTROLLER_GET_NAME'), 500); } @@ -190,9 +201,9 @@ public function publish() \JSession::checkToken() or die(\JText::_('JINVALID_TOKEN')); // Get items to publish from the request. - $cid = $this->input->get('cid', array(), 'array'); - $data = array('publish' => 1, 'unpublish' => 0, 'archive' => 2, 'trash' => -2, 'report' => -3); - $task = $this->getTask(); + $cid = $this->input->get('cid', array(), 'array'); + $data = array('publish' => 1, 'unpublish' => 0, 'archive' => 2, 'trash' => -2, 'report' => -3); + $task = $this->getTask(); $value = ArrayHelper::getValue($data, $task, 0, 'int'); if (empty($cid)) @@ -398,6 +409,6 @@ public function saveOrderAjax() } // Close the application - \JFactory::getApplication()->close(); + $this->app->close(); } } diff --git a/libraries/src/MVC/Controller/BaseController.php b/libraries/src/MVC/Controller/BaseController.php index 7cd753d5c3c5c..c330dff0e3308 100644 --- a/libraries/src/MVC/Controller/BaseController.php +++ b/libraries/src/MVC/Controller/BaseController.php @@ -8,10 +8,13 @@ namespace Joomla\CMS\MVC\Controller; +defined('JPATH_PLATFORM') or die; + +use Joomla\CMS\Application\CMSApplication; +use Joomla\CMS\MVC\Model\BaseDatabaseModel; use Joomla\CMS\MVC\Factory\LegacyFactory; use Joomla\CMS\MVC\Factory\MVCFactoryInterface; - -defined('JPATH_PLATFORM') or die; +use Joomla\CMS\MVC\View\AbstractView; /** * Base class for a Joomla Controller @@ -138,7 +141,7 @@ class BaseController extends \JObject /** * Instance container. * - * @var \JControllerLegacy + * @var static * @since 3.0 */ protected static $instance; @@ -151,6 +154,14 @@ class BaseController extends \JObject */ protected static $views; + /** + * The Application + * + * @var \JApplicationCms|null + * @since 4.0.0 + */ + protected $app; + /** * Adds to the stack of model paths in LIFO order. * @@ -163,7 +174,7 @@ class BaseController extends \JObject */ public static function addModelPath($path, $prefix = '') { - \JModelLegacy::addIncludePath($path, $prefix); + BaseDatabaseModel::addIncludePath($path, $prefix); } /** @@ -225,9 +236,11 @@ public static function createFileName($type, $parts = array()) * @param string $prefix The prefix for the controller. * @param array $config An array of optional constructor options. * - * @return \JControllerLegacy + * @return static * * @since 3.0 + * + * @deprecated 4.0 * @throws \Exception if the controller cannot be loaded. */ public static function getInstance($prefix, $config = array()) @@ -237,7 +250,8 @@ public static function getInstance($prefix, $config = array()) return self::$instance; } - $input = \JFactory::getApplication()->input; + $app = \JFactory::getApplication(); + $input = $app->input; // Get the environment configuration. $basePath = array_key_exists('base_path', $config) ? $config['base_path'] : JPATH_COMPONENT; @@ -310,7 +324,7 @@ public static function getInstance($prefix, $config = array()) } // Instantiate the class, store it to the static container, and return it - return self::$instance = new $class($config); + return self::$instance = new $class($config, null, $app, $input); } /** @@ -320,10 +334,12 @@ public static function getInstance($prefix, $config = array()) * Recognized key values include 'name', 'default_task', 'model_path', and * 'view_path' (this list is not meant to be comprehensive). * @param MVCFactoryInterface $factory The factory. + * @param CMSApplication $app The JApplication for the dispatcher + * @param \JInput $input Input * * @since 3.0 */ - public function __construct($config = array(), MVCFactoryInterface $factory = null) + public function __construct($config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) { $this->methods = array(); $this->message = null; @@ -332,13 +348,14 @@ public function __construct($config = array(), MVCFactoryInterface $factory = nu $this->redirect = null; $this->taskMap = array(); + $this->app = $app ? $app : \JFactory::getApplication(); + $this->input = $input ? $input : $this->app->input; + if (defined('JDEBUG') && JDEBUG) { \JLog::addLogger(array('text_file' => 'jcontroller.log.php'), \JLog::ALL, array('controller')); } - $this->input = \JFactory::getApplication()->input; - // Determine the methods to exclude from the base class. $xMethods = get_class_methods('\JControllerLegacy'); @@ -448,7 +465,7 @@ public function __construct($config = array(), MVCFactoryInterface $factory = nu * @param string $type The path type (e.g. 'model', 'view'). * @param mixed $path The directory string or stream array to search. * - * @return \JControllerLegacy A \JControllerLegacy object to support chaining. + * @return static A \JControllerLegacy object to support chaining. * * @since 3.0 */ @@ -477,7 +494,7 @@ protected function addPath($type, $path) * * @param mixed $path The directory (string) or list of directories (array) to add. * - * @return \JControllerLegacy This object to support chaining. + * @return static This object to support chaining. * * @since 3.0 */ @@ -517,7 +534,7 @@ protected function checkEditId($context, $id) { if ($id) { - $values = (array) \JFactory::getApplication()->getUserState($context . '.id'); + $values = (array) $this->app->getUserState($context . '.id'); $result = in_array((int) $id, $values); @@ -597,9 +614,9 @@ protected function createView($name, $prefix = '', $type = '', $config = array() * you will need to override it in your own controllers. * * @param boolean $cachable If true, the view output will be cached - * @param array $urlparams An array of safe URL parameters and their variable types, for valid values see {@link \JFilterInput::clean()}. + * @param array $urlparams An array of safe url parameters and their variable types, for valid values see {@link \JFilterInput::clean()}. * - * @return \JControllerLegacy A \JControllerLegacy object to support chaining. + * @return static A \JControllerLegacy object to support chaining. * * @since 3.0 */ @@ -628,11 +645,9 @@ public function display($cachable = false, $urlparams = array()) if (is_array($urlparams)) { - $app = \JFactory::getApplication(); - - if (!empty($app->registeredurlparams)) + if (!empty($this->app->registeredurlparams)) { - $registeredurlparams = $app->registeredurlparams; + $registeredurlparams = $this->app->registeredurlparams; } else { @@ -645,7 +660,7 @@ public function display($cachable = false, $urlparams = array()) $registeredurlparams->$key = $value; } - $app->registeredurlparams = $registeredurlparams; + $this->app->registeredurlparams = $registeredurlparams; } try @@ -709,7 +724,7 @@ public function execute($task) * @param string $prefix The class prefix. Optional. * @param array $config Configuration array for model. Optional. * - * @return \JModelLegacy|boolean Model object on success; otherwise false on failure. + * @return BaseDatabaseModel|boolean Model object on success; otherwise false on failure. * * @since 3.0 */ @@ -720,7 +735,7 @@ public function getModel($name = '', $prefix = '', $config = array()) $name = $this->getName(); } - if (empty($prefix)) + if (empty($prefix) && $this->factory instanceof LegacyFactory) { $prefix = $this->model_prefix; } @@ -823,7 +838,7 @@ public function getView($name = '', $type = '', $prefix = '', $config = array()) $name = $this->getName(); } - if (empty($prefix)) + if (empty($prefix) && $this->factory instanceof LegacyFactory) { $prefix = $this->getName() . 'View'; } @@ -855,15 +870,14 @@ public function getView($name = '', $type = '', $prefix = '', $config = array()) */ protected function holdEditId($context, $id) { - $app = \JFactory::getApplication(); - $values = (array) $app->getUserState($context . '.id'); + $values = (array) $this->app->getUserState($context . '.id'); // Add the id to the list if non-zero. if (!empty($id)) { $values[] = (int) $id; $values = array_unique($values); - $app->setUserState($context . '.id', $values); + $this->app->setUserState($context . '.id', $values); if (defined('JDEBUG') && JDEBUG) { @@ -892,13 +906,11 @@ public function redirect() { if ($this->redirect) { - $app = \JFactory::getApplication(); - // Enqueue the redirect message - $app->enqueueMessage($this->message, $this->messageType); + $this->app->enqueueMessage($this->message, $this->messageType); // Execute the redirect - $app->redirect($this->redirect); + $this->app->redirect($this->redirect); } return false; @@ -909,7 +921,7 @@ public function redirect() * * @param string $method The name of the method in the derived class to perform if a named task is not found. * - * @return \JControllerLegacy A \JControllerLegacy object to support chaining. + * @return static A \JControllerLegacy object to support chaining. * * @since 3.0 */ @@ -926,7 +938,7 @@ public function registerDefaultTask($method) * @param string $task The task. * @param string $method The name of the method in the derived class to perform for this task. * - * @return \JControllerLegacy A \JControllerLegacy object to support chaining. + * @return static A \JControllerLegacy object to support chaining. * * @since 3.0 */ @@ -945,7 +957,7 @@ public function registerTask($task, $method) * * @param string $task The task. * - * @return \JControllerLegacy This object to support chaining. + * @return static This object to support chaining. * * @since 3.0 */ @@ -968,8 +980,7 @@ public function unregisterTask($task) */ protected function releaseEditId($context, $id) { - $app = \JFactory::getApplication(); - $values = (array) $app->getUserState($context . '.id'); + $values = (array) $this->app->getUserState($context . '.id'); // Do a strict search of the edit list values. $index = array_search((int) $id, $values, true); @@ -977,7 +988,7 @@ protected function releaseEditId($context, $id) if (is_int($index)) { unset($values[$index]); - $app->setUserState($context . '.id', $values); + $this->app->setUserState($context . '.id', $values); if (defined('JDEBUG') && JDEBUG) { @@ -1059,9 +1070,8 @@ public function checkToken($method = 'post', $redirect = true) $referrer = 'index.php'; } - $app = \JFactory::getApplication(); - $app->enqueueMessage(\JText::_('JINVALID_TOKEN_NOTICE'), 'warning'); - $app->redirect($referrer); + $this->app->enqueueMessage(\JText::_('JINVALID_TOKEN_NOTICE'), 'warning'); + $this->app->redirect($referrer); } return $valid; @@ -1074,7 +1084,7 @@ public function checkToken($method = 'post', $redirect = true) * @param string $msg Message to display on redirect. Optional, defaults to value set internally by controller, if any. * @param string $type Message type. Optional, defaults to 'message' or the type set by a previous call to setMessage. * - * @return \JControllerLegacy This object to support chaining. + * @return static This object to support chaining. * * @since 3.0 */ diff --git a/libraries/src/MVC/Controller/FormController.php b/libraries/src/MVC/Controller/FormController.php index 18d76d4a5fd66..5b886d58487f3 100644 --- a/libraries/src/MVC/Controller/FormController.php +++ b/libraries/src/MVC/Controller/FormController.php @@ -10,6 +10,8 @@ defined('JPATH_PLATFORM') or die; +use Joomla\CMS\Application\CMSApplication; +use Joomla\CMS\Component\ComponentHelper; use Joomla\CMS\MVC\Factory\MVCFactoryInterface; /** @@ -65,19 +67,21 @@ class FormController extends BaseController * * @param array $config An optional associative array of configuration settings. * @param MVCFactoryInterface $factory The factory. + * @param CMSApplication $app The JApplication for the dispatcher + * @param \JInput $input Input * * @see \JControllerLegacy * @since 1.6 * @throws \Exception */ - public function __construct($config = array(), MVCFactoryInterface $factory = null) + public function __construct($config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) { - parent::__construct($config, $factory); + parent::__construct($config, $factory, $app, $input); // Guess the option as com_NameOfController if (empty($this->option)) { - $this->option = 'com_' . strtolower($this->getName()); + $this->option = ComponentHelper::getComponentName($this, $this->getName()); } // Guess the \JText message prefix. Defaults to the option. @@ -91,12 +95,21 @@ public function __construct($config = array(), MVCFactoryInterface $factory = nu { $r = null; - if (!preg_match('/(.*)Controller(.*)/i', get_class($this), $r)) + $match = 'Controller'; + + // If there is a namespace append a backslash + if (strpos(get_class($this), '\\')) + { + $match .= '\\\\'; + } + + if (!preg_match('/(.*)' . $match . '(.*)/i', get_class($this), $r)) { throw new \Exception(\JText::_('JLIB_APPLICATION_ERROR_CONTROLLER_GET_NAME'), 500); } - $this->context = strtolower($r[2]); + // Remove the backslashes and the suffix controller + $this->context = str_replace(array('\\', 'controller'), '', strtolower($r[2])); } // Guess the item view as the context. @@ -245,7 +258,7 @@ protected function allowSave($data, $key = 'id') /** * Method to run batch operations. * - * @param \JModelLegacy $model The model of the component being processed. + * @param BaseDatabaseModel $model The model of the component being processed. * * @return boolean True if successful, false otherwise and internal error is set. * @@ -430,7 +443,7 @@ public function edit($key = null, $urlVar = null) * @param string $prefix The class prefix. Optional. * @param array $config Configuration array for model. Optional. * - * @return \JModelLegacy The model. + * @return BaseDatabaseModel The model. * * @since 1.6 */ @@ -518,8 +531,8 @@ protected function getRedirectToListAppend() * Function that allows child controller access to model data * after the data has been saved. * - * @param \JModelLegacy $model The data model object. - * @param array $validData The validated data. + * @param BaseDatabaseModel $model The data model object. + * @param array $validData The validated data. * * @return void * diff --git a/libraries/src/MVC/Factory/MVCFactory.php b/libraries/src/MVC/Factory/MVCFactory.php index e3a975cd8dbd6..98a3c96fda12d 100644 --- a/libraries/src/MVC/Factory/MVCFactory.php +++ b/libraries/src/MVC/Factory/MVCFactory.php @@ -23,14 +23,16 @@ class MVCFactory implements MVCFactoryInterface /** * The namespace to create the objects from. * - * @var string + * @var string + * @since __DEPLOY_VERSION__ */ private $namespace = null; /** * The application. * - * @var CMSApplication + * @var CMSApplication + * @since __DEPLOY_VERSION__ */ private $application = null; diff --git a/libraries/src/MVC/Model/BaseDatabaseModel.php b/libraries/src/MVC/Model/BaseDatabaseModel.php index 2e81433efa32f..d85d50aa60cd8 100644 --- a/libraries/src/MVC/Model/BaseDatabaseModel.php +++ b/libraries/src/MVC/Model/BaseDatabaseModel.php @@ -10,7 +10,9 @@ defined('JPATH_PLATFORM') or die; +use Joomla\CMS\Component\ComponentHelper; use Joomla\CMS\MVC\Factory\LegacyFactory; +use Joomla\CMS\MVC\Factory\MVCFactory; use Joomla\CMS\MVC\Factory\MVCFactoryInterface; use Joomla\Utilities\ArrayHelper; @@ -233,7 +235,7 @@ public function __construct($config = array(), MVCFactoryInterface $factory = nu throw new \Exception(\JText::_('JLIB_APPLICATION_ERROR_MODEL_GET_NAME'), 500); } - $this->option = 'com_' . strtolower($r[1]); + $this->option = ComponentHelper::getComponentName($this, $r[1]); } // Set the view name @@ -299,6 +301,19 @@ public function __construct($config = array(), MVCFactoryInterface $factory = nu $this->event_clean_cache = 'onContentCleanCache'; } + if (!$factory) + { + $reflect = new \ReflectionClass($this); + if ($reflect->getNamespaceName()) + { + // Guess the root namespace + $ns = explode('\\', $reflect->getNamespaceName()); + $ns = implode('\\', array_slice($ns, 0, 3)); + + $factory = new MVCFactory($ns, \JFactory::getApplication()); + } + } + $this->factory = $factory ? : new LegacyFactory; } @@ -427,7 +442,7 @@ public function getName() throw new \Exception(\JText::_('JLIB_APPLICATION_ERROR_MODEL_GET_NAME'), 500); } - $this->name = strtolower($r[1]); + $this->name = str_replace(array('\\', 'model'), '', strtolower($r[1])); } return $this->name; @@ -476,6 +491,12 @@ public function getTable($name = '', $prefix = 'Table', $options = array()) $name = $this->getName(); } + // We need this ugly code to deal with non-namespaced MVC code + if (empty($prefix) && $this->factory instanceof LegacyFactory) + { + $prefix = 'Table'; + } + if ($table = $this->_createTable($name, $prefix, $options)) { return $table; diff --git a/libraries/src/MVC/Model/FormModel.php b/libraries/src/MVC/Model/FormModel.php index 2f20231928362..bc81e7e08e7aa 100644 --- a/libraries/src/MVC/Model/FormModel.php +++ b/libraries/src/MVC/Model/FormModel.php @@ -220,6 +220,7 @@ protected function loadForm($name, $source = null, $options = array(), $clear = } // Get the form. + \JForm::addFormPath(JPATH_COMPONENT . '/forms'); \JForm::addFormPath(JPATH_COMPONENT . '/models/forms'); \JForm::addFieldPath(JPATH_COMPONENT . '/models/fields'); \JForm::addFormPath(JPATH_COMPONENT . '/model/form'); diff --git a/libraries/src/MVC/Model/ListModel.php b/libraries/src/MVC/Model/ListModel.php index eaea27122709c..490fd6c018549 100644 --- a/libraries/src/MVC/Model/ListModel.php +++ b/libraries/src/MVC/Model/ListModel.php @@ -348,9 +348,9 @@ public function getFilterForm($data = array(), $loadData = true) { $classNameParts = explode('Model', get_called_class()); - if (count($classNameParts) == 2) + if (count($classNameParts) >= 2) { - $this->filterFormName = 'filter_' . strtolower($classNameParts[1]); + $this->filterFormName = 'filter_' . str_replace('\\', '', strtolower($classNameParts[1])); } } @@ -392,6 +392,7 @@ protected function loadForm($name, $source = null, $options = array(), $clear = } // Get the form. + \JForm::addFormPath(JPATH_COMPONENT . '/forms'); \JForm::addFormPath(JPATH_COMPONENT . '/models/forms'); \JForm::addFieldPath(JPATH_COMPONENT . '/models/fields'); diff --git a/libraries/src/MVC/View/HtmlView.php b/libraries/src/MVC/View/HtmlView.php index b5950212d3442..853ca81a32ace 100644 --- a/libraries/src/MVC/View/HtmlView.php +++ b/libraries/src/MVC/View/HtmlView.php @@ -182,14 +182,26 @@ public function __construct($config = array()) // User-defined dirs $this->_setPath('template', $config['template_path']); } - elseif (is_dir($this->_basePath . '/view')) + elseif (is_dir($this->_basePath . '/View/' . $this->getName() . '/tmpl')) + { + $this->_setPath('template', $this->_basePath . '/View/' . $this->getName() . '/tmpl'); + } + elseif (is_dir($this->_basePath . '/view/' . $this->getName() . '/tmpl')) { $this->_setPath('template', $this->_basePath . '/view/' . $this->getName() . '/tmpl'); } - else + elseif (is_dir($this->_basePath . '/tmpl/' . $this->getName())) + { + $this->_setPath('template', $this->_basePath . '/tmpl/' . $this->getName()); + } + elseif (is_dir($this->_basePath . '/views/' . $this->getName() . '/tmpl')) { $this->_setPath('template', $this->_basePath . '/views/' . $this->getName() . '/tmpl'); } + else + { + $this->_setPath('template', $this->_basePath . '/views/' . $this->getName()); + } // Set the default helper search path if (array_key_exists('helper_path', $config)) @@ -490,15 +502,31 @@ public function getName() { if (empty($this->_name)) { - $classname = get_class($this); - $viewpos = strpos($classname, 'View'); + $reflection = new \ReflectionClass($this); + if ($viewNamespace = $reflection->getNamespaceName()) + { + $pos = strrpos($viewNamespace, '\\'); + + if ($pos !== false) + { + $this->_name = strtolower(substr($viewNamespace, $pos + 1)); + } + } + else + { + $className = get_class($this); + $viewPos = strpos($className, 'View'); - if ($viewpos === false) + if ($viewPos != false) + { + $this->_name = strtolower(substr($className, $viewPos + 4)); + } + } + + if (empty($this->_name)) { throw new \Exception(\JText::_('JLIB_APPLICATION_ERROR_VIEW_GET_NAME'), 500); } - - $this->_name = strtolower(substr($classname, $viewpos + 4)); } return $this->_name; diff --git a/tests/unit/suites/libraries/legacy/model/JModelLegacyTest.php b/tests/unit/suites/libraries/legacy/model/JModelLegacyTest.php index 66a513503280b..3b3749ab68177 100644 --- a/tests/unit/suites/libraries/legacy/model/JModelLegacyTest.php +++ b/tests/unit/suites/libraries/legacy/model/JModelLegacyTest.php @@ -397,7 +397,7 @@ public function testGetNameOfClassWithLowercaseModelInName() { // Test creating fixture with model in class name, currently reflects an inconsistency in the codebase $this->fixture = JModelLegacy::getInstance('Room', 'RemodelModel'); - $this->assertEquals('modelroom', $this->fixture->getName()); + $this->assertEquals('room', $this->fixture->getName()); $this->assertEquals('com_remodel', TestReflection::getValue($this->fixture, 'option')); } diff --git a/tests/unit/suites/libraries/legacy/model/JModelListTest.php b/tests/unit/suites/libraries/legacy/model/JModelListTest.php index ab02ddf818d3b..33f6608c5cabe 100644 --- a/tests/unit/suites/libraries/legacy/model/JModelListTest.php +++ b/tests/unit/suites/libraries/legacy/model/JModelListTest.php @@ -81,7 +81,7 @@ public function testFilterFieldsIsSetInConstructor() */ public function testContextIsSetInConstructor() { - $this->assertSame("com_joomla\cms\mvc\model\list.\listmodel", TestReflection::getValue($this->object, 'context')); + $this->assertSame("com_mvc.list", TestReflection::getValue($this->object, 'context')); } /** @@ -134,7 +134,7 @@ public function testGetStoreIdIncludesAllStates() $this->object->setState('list.ordering', 'enabled'); $this->object->setState('list.direction', 'ASC'); - $expectedString = "com_joomla\cms\mvc\model\list.\listmodel:1:0:100:enabled:ASC"; + $expectedString = "com_mvc.list:1:0:100:enabled:ASC"; $this->assertSame(md5($expectedString), $method->invokeArgs($this->object, array('1'))); } @@ -401,7 +401,7 @@ public function testListInfoIsAppendedToFormData() $applicationMock->expects($this->once()) ->method('getUserState') ->with( - $this->equalTo('com_joomla\cms\mvc\model\list.\listmodel'), + $this->equalTo('com_mvc.list'), $this->equalTo(new stdClass) ) ->will( @@ -451,7 +451,7 @@ public function testLoadFormDataDoesNotOverwriteListInfo() $applicationMock->expects($this->once()) ->method('getUserState') ->with( - $this->equalTo('com_joomla\cms\mvc\model\list.\listmodel'), + $this->equalTo('com_mvc.list'), $this->equalTo(new stdClass) ) ->will($this->returnValue($data)); @@ -485,12 +485,12 @@ public function testPopulateStateAppliesFilters() $applicationMock = $this->getMockCmsApp(); $applicationMock->method('getUserStateFromRequest') ->withConsecutive( - array($this->equalTo('com_joomla\cms\mvc\model\list.\listmodel.filter'), $this->equalTo('filter'), $this->equalTo(array()), $this->equalTo('array')), - array($this->equalTo('com_joomla\cms\mvc\model\list.\listmodel.list'), $this->equalTo('list'), $this->equalTo(array()), $this->equalTo('array')), + array($this->equalTo('com_mvc.list.filter'), $this->equalTo('filter'), $this->equalTo(array()), $this->equalTo('array')), + array($this->equalTo('com_mvc.list.list'), $this->equalTo('list'), $this->equalTo(array()), $this->equalTo('array')), array($this->equalTo('global.list.limit'), $this->equalTo('limit'), $this->equalTo(null), $this->equalTo('uint')), - array($this->equalTo('com_joomla\cms\mvc\model\list.\listmodel.ordercol'), $this->equalTo('filter_order'), $this->equalTo('col'), $this->equalTo('none')), - array($this->equalTo('com_joomla\cms\mvc\model\list.\listmodel.orderdirn'), $this->equalTo('filter_order_Dir'), $this->equalTo('ASC'), $this->equalTo('none')), - array($this->equalTo('com_joomla\cms\mvc\model\list.\listmodel.limitstart'), $this->equalTo('limitstart'), $this->equalTo(0)) + array($this->equalTo('com_mvc.list.ordercol'), $this->equalTo('filter_order'), $this->equalTo('col'), $this->equalTo('none')), + array($this->equalTo('com_mvc.list.orderdirn'), $this->equalTo('filter_order_Dir'), $this->equalTo('ASC'), $this->equalTo('none')), + array($this->equalTo('com_mvc.list.limitstart'), $this->equalTo('limitstart'), $this->equalTo(0)) ) ->will( $this->onConsecutiveCalls( @@ -539,12 +539,12 @@ public function testPopulateStateUsesWhitelistForOrderColumn() $applicationMock = $this->getMockCmsApp(); $applicationMock->method('getUserStateFromRequest') ->withConsecutive( - array($this->equalTo('com_joomla\cms\mvc\model\list.\listmodel.filter'), $this->equalTo('filter'), $this->equalTo(array()), $this->equalTo('array')), - array($this->equalTo('com_joomla\cms\mvc\model\list.\listmodel.list'), $this->equalTo('list'), $this->equalTo(array()), $this->equalTo('array')), + array($this->equalTo('com_mvc.list.filter'), $this->equalTo('filter'), $this->equalTo(array()), $this->equalTo('array')), + array($this->equalTo('com_mvc.list.list'), $this->equalTo('list'), $this->equalTo(array()), $this->equalTo('array')), array($this->equalTo('global.list.limit'), $this->equalTo('limit'), $this->equalTo(null), $this->equalTo('uint')), - array($this->equalTo('com_joomla\cms\mvc\model\list.\listmodel.ordercol'), $this->equalTo('filter_order'), $this->equalTo('inwhitelist'), $this->equalTo('none')), - array($this->equalTo('com_joomla\cms\mvc\model\list.\listmodel.orderdirn'), $this->equalTo('filter_order_Dir'), $this->equalTo('ASC'), $this->equalTo('none')), - array($this->equalTo('com_joomla\cms\mvc\model\list.\listmodel.limitstart'), $this->equalTo('limitstart'), $this->equalTo(0)) + array($this->equalTo('com_mvc.list.ordercol'), $this->equalTo('filter_order'), $this->equalTo('inwhitelist'), $this->equalTo('none')), + array($this->equalTo('com_mvc.list.orderdirn'), $this->equalTo('filter_order_Dir'), $this->equalTo('ASC'), $this->equalTo('none')), + array($this->equalTo('com_mvc.list.limitstart'), $this->equalTo('limitstart'), $this->equalTo(0)) ) ->will( $this->onConsecutiveCalls( @@ -591,12 +591,12 @@ public function testPopulateStateFixedInvalidOrderDirection() $applicationMock = $this->getMockCmsApp(); $applicationMock->method('getUserStateFromRequest') ->withConsecutive( - array($this->equalTo('com_joomla\cms\mvc\model\list.\listmodel.filter'), $this->equalTo('filter'), $this->equalTo(array()), $this->equalTo('array')), - array($this->equalTo('com_joomla\cms\mvc\model\list.\listmodel.list'), $this->equalTo('list'), $this->equalTo(array()), $this->equalTo('array')), + array($this->equalTo('com_mvc.list.filter'), $this->equalTo('filter'), $this->equalTo(array()), $this->equalTo('array')), + array($this->equalTo('com_mvc.list.list'), $this->equalTo('list'), $this->equalTo(array()), $this->equalTo('array')), array($this->equalTo('global.list.limit'), $this->equalTo('limit'), $this->equalTo(null), $this->equalTo('uint')), - array($this->equalTo('com_joomla\cms\mvc\model\list.\listmodel.ordercol'), $this->equalTo('filter_order'), $this->equalTo('col'), $this->equalTo('none')), - array($this->equalTo('com_joomla\cms\mvc\model\list.\listmodel.orderdirn'), $this->equalTo('filter_order_Dir'), $this->equalTo('ASC'), $this->equalTo('none')), - array($this->equalTo('com_joomla\cms\mvc\model\list.\listmodel.limitstart'), $this->equalTo('limitstart'), $this->equalTo(0)) + array($this->equalTo('com_mvc.list.ordercol'), $this->equalTo('filter_order'), $this->equalTo('col'), $this->equalTo('none')), + array($this->equalTo('com_mvc.list.orderdirn'), $this->equalTo('filter_order_Dir'), $this->equalTo('ASC'), $this->equalTo('none')), + array($this->equalTo('com_mvc.list.limitstart'), $this->equalTo('limitstart'), $this->equalTo(0)) ) ->will( $this->onConsecutiveCalls( @@ -639,12 +639,12 @@ public function testPopulateStateSupportsOldFilterOrder() $applicationMock = $this->getMockCmsApp(); $applicationMock->method('getUserStateFromRequest') ->withConsecutive( - array($this->equalTo('com_joomla\cms\mvc\model\list.\listmodel.filter'), $this->equalTo('filter'), $this->equalTo(array()), $this->equalTo('array')), - array($this->equalTo('com_joomla\cms\mvc\model\list.\listmodel.list'), $this->equalTo('list'), $this->equalTo(array()), $this->equalTo('array')), + array($this->equalTo('com_mvc.list.filter'), $this->equalTo('filter'), $this->equalTo(array()), $this->equalTo('array')), + array($this->equalTo('com_mvc.list.list'), $this->equalTo('list'), $this->equalTo(array()), $this->equalTo('array')), array($this->equalTo('global.list.limit'), $this->equalTo('limit'), $this->equalTo(null), $this->equalTo('uint')), - array($this->equalTo('com_joomla\cms\mvc\model\list.\listmodel.ordercol'), $this->equalTo('filter_order'), $this->equalTo('col'), $this->equalTo('none')), - array($this->equalTo('com_joomla\cms\mvc\model\list.\listmodel.orderdirn'), $this->equalTo('filter_order_Dir'), $this->equalTo('ASC'), $this->equalTo('none')), - array($this->equalTo('com_joomla\cms\mvc\model\list.\listmodel.limitstart'), $this->equalTo('limitstart'), $this->equalTo(0)) + array($this->equalTo('com_mvc.list.ordercol'), $this->equalTo('filter_order'), $this->equalTo('col'), $this->equalTo('none')), + array($this->equalTo('com_mvc.list.orderdirn'), $this->equalTo('filter_order_Dir'), $this->equalTo('ASC'), $this->equalTo('none')), + array($this->equalTo('com_mvc.list.limitstart'), $this->equalTo('limitstart'), $this->equalTo(0)) ) ->will( $this->onConsecutiveCalls( @@ -703,9 +703,9 @@ public function testPopulateStateSupportsListFilters() $applicationMock = $this->getMockCmsApp(); $applicationMock->method('getUserStateFromRequest') ->withConsecutive( - array($this->equalTo('com_joomla\cms\mvc\model\list.\listmodel.filter'), $this->equalTo('filter'), $this->equalTo(array()), $this->equalTo('array')), - array($this->equalTo('com_joomla\cms\mvc\model\list.\listmodel.list'), $this->equalTo('list'), $this->equalTo(array()), $this->equalTo('array')), - array($this->equalTo('com_joomla\cms\mvc\model\list.\listmodel.limitstart'), $this->equalTo('limitstart'), $this->equalTo(0)) + array($this->equalTo('com_mvc.list.filter'), $this->equalTo('filter'), $this->equalTo(array()), $this->equalTo('array')), + array($this->equalTo('com_mvc.list.list'), $this->equalTo('list'), $this->equalTo(array()), $this->equalTo('array')), + array($this->equalTo('com_mvc.list.limitstart'), $this->equalTo('limitstart'), $this->equalTo(0)) ) ->will( $this->onConsecutiveCalls( @@ -756,9 +756,9 @@ public function testPopulateStateSupportsFullordering() $applicationMock = $this->getMockCmsApp(); $applicationMock->method('getUserStateFromRequest') ->withConsecutive( - array($this->equalTo('com_joomla\cms\mvc\model\list.\listmodel.filter'), $this->equalTo('filter'), $this->equalTo(array()), $this->equalTo('array')), - array($this->equalTo('com_joomla\cms\mvc\model\list.\listmodel.list'), $this->equalTo('list'), $this->equalTo(array()), $this->equalTo('array')), - array($this->equalTo('com_joomla\cms\mvc\model\list.\listmodel.limitstart'), $this->equalTo('limitstart'), $this->equalTo(0)) + array($this->equalTo('com_mvc.list.filter'), $this->equalTo('filter'), $this->equalTo(array()), $this->equalTo('array')), + array($this->equalTo('com_mvc.list.list'), $this->equalTo('list'), $this->equalTo(array()), $this->equalTo('array')), + array($this->equalTo('com_mvc.list.limitstart'), $this->equalTo('limitstart'), $this->equalTo(0)) ) ->will( $this->onConsecutiveCalls( @@ -806,9 +806,9 @@ public function testPopulateStateFixesInvalidFullordering() $applicationMock = $this->getMockCmsApp(); $applicationMock->method('getUserStateFromRequest') ->withConsecutive( - array($this->equalTo('com_joomla\cms\mvc\model\list.\listmodel.filter'), $this->equalTo('filter'), $this->equalTo(array()), $this->equalTo('array')), - array($this->equalTo('com_joomla\cms\mvc\model\list.\listmodel.list'), $this->equalTo('list'), $this->equalTo(array()), $this->equalTo('array')), - array($this->equalTo('com_joomla\cms\mvc\model\list.\listmodel.limitstart'), $this->equalTo('limitstart'), $this->equalTo(0)) + array($this->equalTo('com_mvc.list.filter'), $this->equalTo('filter'), $this->equalTo(array()), $this->equalTo('array')), + array($this->equalTo('com_mvc.list.list'), $this->equalTo('list'), $this->equalTo(array()), $this->equalTo('array')), + array($this->equalTo('com_mvc.list.limitstart'), $this->equalTo('limitstart'), $this->equalTo(0)) ) ->will( $this->onConsecutiveCalls( @@ -854,9 +854,9 @@ public function testPopulateStateFixesInvalidOrderValuesFromList() $applicationMock = $this->getMockCmsApp(); $applicationMock->method('getUserStateFromRequest') ->withConsecutive( - array($this->equalTo('com_joomla\cms\mvc\model\list.\listmodel.filter'), $this->equalTo('filter'), $this->equalTo(array()), $this->equalTo('array')), - array($this->equalTo('com_joomla\cms\mvc\model\list.\listmodel.list'), $this->equalTo('list'), $this->equalTo(array()), $this->equalTo('array')), - array($this->equalTo('com_joomla\cms\mvc\model\list.\listmodel.limitstart'), $this->equalTo('limitstart'), $this->equalTo(0)) + array($this->equalTo('com_mvc.list.filter'), $this->equalTo('filter'), $this->equalTo(array()), $this->equalTo('array')), + array($this->equalTo('com_mvc.list.list'), $this->equalTo('list'), $this->equalTo(array()), $this->equalTo('array')), + array($this->equalTo('com_mvc.list.limitstart'), $this->equalTo('limitstart'), $this->equalTo(0)) ) ->will( $this->onConsecutiveCalls(